|
41 | 41 | pick_info, |
42 | 42 | pick_types, |
43 | 43 | ) |
44 | | -from .._fiff.proj import _has_eeg_average_ref_proj, setup_proj |
| 44 | +from .._fiff.proj import setup_proj |
45 | 45 | from .._fiff.reference import add_reference_channels, set_eeg_reference |
46 | 46 | from .._fiff.tag import _rename_list |
47 | 47 | from ..bem import _check_origin |
|
61 | 61 | warn, |
62 | 62 | ) |
63 | 63 |
|
| 64 | +_ALLOWED_INTERPOLATION_MODES = ("accurate", "fast") |
| 65 | + |
64 | 66 |
|
65 | 67 | def _get_meg_system(info): |
66 | 68 | """Educated guess for the helmet type based on channels.""" |
@@ -968,161 +970,102 @@ def interpolate_bads( |
968 | 970 |
|
969 | 971 | return self |
970 | 972 |
|
971 | | - def interpolate_to(self, sensors, origin="auto", method="spline", reg=0.0): |
972 | | - """Interpolate EEG data onto a new montage. |
| 973 | + def interpolate_to( |
| 974 | + self, sensors, origin="auto", method=None, mode="accurate", reg=0.0 |
| 975 | + ): |
| 976 | + """Interpolate data onto a new sensor configuration. |
973 | 977 |
|
974 | | - .. warning:: |
975 | | - Be careful, only EEG channels are interpolated. Other channel types are |
976 | | - not interpolated. |
| 978 | + This method can interpolate EEG data onto a new montage or transform |
| 979 | + MEG data to a different sensor configuration (e.g., Neuromag to CTF). |
977 | 980 |
|
978 | 981 | Parameters |
979 | 982 | ---------- |
980 | | - sensors : DigMontage |
981 | | - The target montage containing channel positions to interpolate onto. |
| 983 | + sensors : DigMontage | str |
| 984 | + For EEG: A DigMontage object containing target channel positions. |
| 985 | + For MEG: A string specifying the target MEG system. Currently |
| 986 | + supported: ``'neuromag'``, ``'ctf151'`` or ``'ctf275'``. |
982 | 987 | origin : array-like, shape (3,) | str |
983 | 988 | Origin of the sphere in the head coordinate frame and in meters. |
984 | 989 | Can be ``'auto'`` (default), which means a head-digitization-based |
985 | | - origin fit. |
986 | | - method : str |
987 | | - Method to use for EEG channels. |
988 | | - Supported methods are 'spline' (default) and 'MNE'. |
| 990 | + origin fit. Used for both EEG and MEG interpolation. |
| 991 | + method : str | None |
| 992 | + Interpolation method to use. |
| 993 | + For EEG: ``'spline'`` (default, same as None) or ``'MNE'``. |
| 994 | + For MEG: ``'MNE'`` (default, same as None). |
| 995 | +
|
| 996 | + .. versionchanged:: 1.10.0 |
| 997 | + Added support for MEG interpolation. |
| 998 | + mode : str |
| 999 | + Either ``'accurate'`` (default) or ``'fast'``, determines the |
| 1000 | + quality of the Legendre polynomial expansion used for |
| 1001 | + interpolation of MEG channels using the minimum-norm method. |
| 1002 | + Only used for MEG interpolation. |
989 | 1003 | reg : float |
990 | | - The regularization parameter for the interpolation method |
991 | | - (only used when the method is 'spline'). |
| 1004 | + The regularization parameter for the interpolation method. |
| 1005 | + Only used when ``method='spline'`` for EEG channels. |
992 | 1006 |
|
993 | 1007 | Returns |
994 | 1008 | ------- |
995 | 1009 | inst : instance of Raw, Epochs, or Evoked |
996 | | - The instance with updated channel locations and data. |
| 1010 | + A new instance with interpolated data and updated channel |
| 1011 | + information. |
997 | 1012 |
|
998 | 1013 | Notes |
999 | 1014 | ----- |
1000 | | - This method is useful for standardizing EEG layouts across datasets. |
1001 | | - However, some attributes may be lost after interpolation. |
| 1015 | + **For EEG data:** |
1002 | 1016 |
|
1003 | | - .. versionadded:: 1.10.0 |
1004 | | - """ |
1005 | | - from ..epochs import BaseEpochs, EpochsArray |
1006 | | - from ..evoked import Evoked, EvokedArray |
1007 | | - from ..forward._field_interpolation import _map_meg_or_eeg_channels |
1008 | | - from ..io import RawArray |
1009 | | - from ..io.base import BaseRaw |
1010 | | - from .interpolation import _make_interpolation_matrix |
1011 | | - from .montage import DigMontage |
| 1017 | + This method interpolates EEG channels onto a new montage using |
| 1018 | + spherical splines or minimum-norm estimation. Non-EEG channels |
| 1019 | + are preserved without modification. |
1012 | 1020 |
|
1013 | | - # Check that the method option is valid. |
1014 | | - _check_option("method", method, ["spline", "MNE"]) |
1015 | | - _validate_type(sensors, DigMontage, "sensors") |
| 1021 | + **For MEG data:** |
1016 | 1022 |
|
1017 | | - # Get target positions from the montage |
1018 | | - ch_pos = sensors.get_positions().get("ch_pos", {}) |
1019 | | - target_ch_names = list(ch_pos.keys()) |
1020 | | - if not target_ch_names: |
1021 | | - raise ValueError( |
1022 | | - "The provided sensors configuration has no channel positions." |
1023 | | - ) |
| 1023 | + This method transforms MEG data to a different sensor configuration |
| 1024 | + using field interpolation. |
1024 | 1025 |
|
1025 | | - # Get original channel order |
1026 | | - orig_names = self.info["ch_names"] |
1027 | | - |
1028 | | - # Identify EEG channel |
1029 | | - picks_good_eeg = pick_types(self.info, meg=False, eeg=True, exclude="bads") |
1030 | | - if len(picks_good_eeg) == 0: |
1031 | | - raise ValueError("No good EEG channels available for interpolation.") |
1032 | | - # Also get the full list of EEG channel indices (including bad channels) |
1033 | | - picks_remove_eeg = pick_types(self.info, meg=False, eeg=True, exclude=[]) |
1034 | | - eeg_names_orig = [orig_names[i] for i in picks_remove_eeg] |
1035 | | - |
1036 | | - # Identify non-EEG channels in original order |
1037 | | - non_eeg_names_ordered = [ch for ch in orig_names if ch not in eeg_names_orig] |
1038 | | - |
1039 | | - # Create destination info for new EEG channels |
1040 | | - sfreq = self.info["sfreq"] |
1041 | | - info_interp = create_info( |
1042 | | - ch_names=target_ch_names, |
1043 | | - sfreq=sfreq, |
1044 | | - ch_types=["eeg"] * len(target_ch_names), |
1045 | | - ) |
1046 | | - info_interp.set_montage(sensors) |
1047 | | - info_interp["bads"] = [ch for ch in self.info["bads"] if ch in target_ch_names] |
1048 | | - # Do not assign "projs" directly. |
1049 | | - |
1050 | | - # Compute the interpolation mapping |
1051 | | - if method == "spline": |
1052 | | - origin_val = _check_origin(origin, self.info) |
1053 | | - pos_from = self.info._get_channel_positions(picks_good_eeg) - origin_val |
1054 | | - pos_to = np.stack(list(ch_pos.values()), axis=0) |
1055 | | - |
1056 | | - def _check_pos_sphere(pos): |
1057 | | - d = np.linalg.norm(pos, axis=-1) |
1058 | | - d_norm = np.mean(d / np.mean(d)) |
1059 | | - if np.abs(1.0 - d_norm) > 0.1: |
1060 | | - warn("Your spherical fit is poor; interpolation may be inaccurate.") |
1061 | | - |
1062 | | - _check_pos_sphere(pos_from) |
1063 | | - _check_pos_sphere(pos_to) |
1064 | | - mapping = _make_interpolation_matrix(pos_from, pos_to, alpha=reg) |
| 1026 | + Common use cases for MEG transformation: |
1065 | 1027 |
|
1066 | | - else: |
1067 | | - assert method == "MNE" |
1068 | | - info_eeg = pick_info(self.info, picks_good_eeg) |
1069 | | - # If the original info has an average EEG reference projector but |
1070 | | - # the destination info does not, |
1071 | | - # update info_interp via a temporary RawArray. |
1072 | | - if _has_eeg_average_ref_proj(self.info) and not _has_eeg_average_ref_proj( |
1073 | | - info_interp |
1074 | | - ): |
1075 | | - # Create dummy data: shape (n_channels, 1) |
1076 | | - temp_data = np.zeros((len(info_interp["ch_names"]), 1)) |
1077 | | - temp_raw = RawArray(temp_data, info_interp, first_samp=0) |
1078 | | - # Using the public API, add an average reference projector. |
1079 | | - temp_raw.set_eeg_reference( |
1080 | | - ref_channels="average", projection=True, verbose=False |
1081 | | - ) |
1082 | | - # Extract the updated info. |
1083 | | - info_interp = temp_raw.info |
1084 | | - mapping = _map_meg_or_eeg_channels( |
1085 | | - info_eeg, info_interp, mode="accurate", origin=origin |
1086 | | - ) |
| 1028 | + - Transform Neuromag data to CTF sensor layout for comparison |
| 1029 | + - Transform CTF data to Neuromag sensor layout |
| 1030 | + - Simulate what data would look like on a different MEG system |
1087 | 1031 |
|
1088 | | - # Interpolate EEG data |
1089 | | - data_good = self.get_data(picks=picks_good_eeg) |
1090 | | - data_interp = mapping @ data_good |
| 1032 | + .. warning:: |
| 1033 | + MEG field interpolation assumes that the head position relative |
| 1034 | + to the sensors is similar between systems. Large differences in |
| 1035 | + head position may affect interpolation accuracy. |
1091 | 1036 |
|
1092 | | - # Create a new instance for the interpolated EEG channels |
1093 | | - # TODO: Creating a new instance leads to a loss of information. |
1094 | | - # We should consider updating the existing instance in the future |
1095 | | - # by 1) drop channels, 2) add channels, 3) re-order channels. |
1096 | | - if isinstance(self, BaseRaw): |
1097 | | - inst_interp = RawArray(data_interp, info_interp, first_samp=self.first_samp) |
1098 | | - elif isinstance(self, BaseEpochs): |
1099 | | - inst_interp = EpochsArray(data_interp, info_interp) |
1100 | | - else: |
1101 | | - assert isinstance(self, Evoked) |
1102 | | - inst_interp = EvokedArray(data_interp, info_interp) |
1103 | | - |
1104 | | - # Merge only if non-EEG channels exist |
1105 | | - if not non_eeg_names_ordered: |
1106 | | - return inst_interp |
1107 | | - |
1108 | | - inst_non_eeg = self.copy().pick(non_eeg_names_ordered).load_data() |
1109 | | - inst_out = inst_non_eeg.add_channels([inst_interp], force_update_info=True) |
1110 | | - |
1111 | | - # Reorder channels |
1112 | | - # Insert the entire new EEG block at the position of the first EEG channel. |
1113 | | - orig_names_arr = np.array(orig_names) |
1114 | | - mask_eeg = np.isin(orig_names_arr, eeg_names_orig) |
1115 | | - if mask_eeg.any(): |
1116 | | - first_eeg_index = np.where(mask_eeg)[0][0] |
1117 | | - pre = orig_names_arr[:first_eeg_index] |
1118 | | - new_eeg = np.array(info_interp["ch_names"]) |
1119 | | - post = orig_names_arr[first_eeg_index:] |
1120 | | - post = post[~np.isin(orig_names_arr[first_eeg_index:], eeg_names_orig)] |
1121 | | - new_order = np.concatenate((pre, new_eeg, post)).tolist() |
| 1037 | + .. versionadded:: 1.10.0 |
| 1038 | + .. versionchanged:: 1.12.0 |
| 1039 | + Added support for MEG interpolation to canonical systems. |
| 1040 | + """ |
| 1041 | + from .interpolation import _interpolate_to_eeg, _interpolate_to_meg |
| 1042 | + from .montage import DigMontage |
| 1043 | + |
| 1044 | + _check_preload(self, "interpolation") |
| 1045 | + |
| 1046 | + # Determine if we're doing EEG or MEG interpolation |
| 1047 | + _validate_type(sensors, (str, DigMontage), "sensors") |
| 1048 | + _validate_type(method, (str, None), "method") |
| 1049 | + is_meg_interpolation = isinstance(sensors, str) |
| 1050 | + is_eeg_interpolation = isinstance(sensors, DigMontage) |
| 1051 | + |
| 1052 | + if is_eeg_interpolation: |
| 1053 | + valid_methods = ["spline", "MNE"] |
| 1054 | + func = partial(_interpolate_to_eeg, method=method, reg=reg) |
| 1055 | + kind = "eeg" |
1122 | 1056 | else: |
1123 | | - new_order = orig_names |
1124 | | - inst_out.reorder_channels(new_order) |
1125 | | - return inst_out |
| 1057 | + assert is_meg_interpolation |
| 1058 | + valid_methods = ["MNE"] |
| 1059 | + _check_option("sensors", sensors, ["neuromag", "ctf151", "ctf275"]) |
| 1060 | + func = partial(_interpolate_to_meg, mode=mode) |
| 1061 | + kind = "meg" |
| 1062 | + |
| 1063 | + if method is None: |
| 1064 | + method = _handle_default("interpolation_method")[kind] |
| 1065 | + _check_option("mode", mode, _ALLOWED_INTERPOLATION_MODES) |
| 1066 | + extra = f"when doing {kind.upper()} interpolation" |
| 1067 | + _check_option("method", method, valid_methods, extra=extra) |
| 1068 | + return func(self, sensors, origin) |
1126 | 1069 |
|
1127 | 1070 |
|
1128 | 1071 | @verbose |
|
0 commit comments