# Authors: The MNE-Python contributors. # License: BSD-3-Clause # Copyright the MNE-Python contributors. import re from copy import deepcopy import numpy as np from ..utils import ( _check_option, _ensure_int, _validate_type, fill_doc, logger, verbose, ) from .constants import FIFF def get_channel_type_constants(include_defaults=False): """Return all known channel types, and associated FIFF constants. Parameters ---------- include_defaults : bool Whether to include default values for "unit" and "coil_type" for all entries (see Notes). Defaults are generally based on values normally present for a VectorView MEG system. Defaults to ``False``. Returns ------- channel_types : dict The keys are channel type strings, and the values are dictionaries of FIFF constants for "kind", and possibly "unit" and "coil_type". Notes ----- Values which might vary within a channel type across real data recordings are excluded unless ``include_defaults=True``. For example, "ref_meg" channels may have coil type ``FIFFV_COIL_MAGNES_OFFDIAG_REF_GRAD``, ``FIFFV_COIL_VV_MAG_T3``, etc (depending on the recording system), so no "coil_type" entry is given for "ref_meg" unless ``include_defaults`` is requested. """ base = dict( grad=dict(kind=FIFF.FIFFV_MEG_CH, unit=FIFF.FIFF_UNIT_T_M), mag=dict(kind=FIFF.FIFFV_MEG_CH, unit=FIFF.FIFF_UNIT_T), ref_meg=dict(kind=FIFF.FIFFV_REF_MEG_CH), eeg=dict( kind=FIFF.FIFFV_EEG_CH, unit=FIFF.FIFF_UNIT_V, coil_type=FIFF.FIFFV_COIL_EEG ), seeg=dict( kind=FIFF.FIFFV_SEEG_CH, unit=FIFF.FIFF_UNIT_V, coil_type=FIFF.FIFFV_COIL_EEG, ), dbs=dict( kind=FIFF.FIFFV_DBS_CH, unit=FIFF.FIFF_UNIT_V, coil_type=FIFF.FIFFV_COIL_EEG ), ecog=dict( kind=FIFF.FIFFV_ECOG_CH, unit=FIFF.FIFF_UNIT_V, coil_type=FIFF.FIFFV_COIL_EEG, ), eog=dict(kind=FIFF.FIFFV_EOG_CH, unit=FIFF.FIFF_UNIT_V), emg=dict(kind=FIFF.FIFFV_EMG_CH, unit=FIFF.FIFF_UNIT_V), ecg=dict(kind=FIFF.FIFFV_ECG_CH, unit=FIFF.FIFF_UNIT_V), resp=dict(kind=FIFF.FIFFV_RESP_CH, unit=FIFF.FIFF_UNIT_V), bio=dict(kind=FIFF.FIFFV_BIO_CH, unit=FIFF.FIFF_UNIT_V), misc=dict(kind=FIFF.FIFFV_MISC_CH, unit=FIFF.FIFF_UNIT_V), stim=dict(kind=FIFF.FIFFV_STIM_CH), exci=dict(kind=FIFF.FIFFV_EXCI_CH), syst=dict(kind=FIFF.FIFFV_SYST_CH), ias=dict(kind=FIFF.FIFFV_IAS_CH), gof=dict(kind=FIFF.FIFFV_GOODNESS_FIT), dipole=dict(kind=FIFF.FIFFV_DIPOLE_WAVE), chpi=dict( kind=[ FIFF.FIFFV_QUAT_0, FIFF.FIFFV_QUAT_1, FIFF.FIFFV_QUAT_2, FIFF.FIFFV_QUAT_3, FIFF.FIFFV_QUAT_4, FIFF.FIFFV_QUAT_5, FIFF.FIFFV_QUAT_6, FIFF.FIFFV_HPI_G, FIFF.FIFFV_HPI_ERR, FIFF.FIFFV_HPI_MOV, ] ), fnirs_cw_amplitude=dict( kind=FIFF.FIFFV_FNIRS_CH, unit=FIFF.FIFF_UNIT_V, coil_type=FIFF.FIFFV_COIL_FNIRS_CW_AMPLITUDE, ), fnirs_fd_ac_amplitude=dict( kind=FIFF.FIFFV_FNIRS_CH, unit=FIFF.FIFF_UNIT_V, coil_type=FIFF.FIFFV_COIL_FNIRS_FD_AC_AMPLITUDE, ), fnirs_fd_phase=dict( kind=FIFF.FIFFV_FNIRS_CH, unit=FIFF.FIFF_UNIT_RAD, coil_type=FIFF.FIFFV_COIL_FNIRS_FD_PHASE, ), fnirs_od=dict(kind=FIFF.FIFFV_FNIRS_CH, coil_type=FIFF.FIFFV_COIL_FNIRS_OD), hbo=dict( kind=FIFF.FIFFV_FNIRS_CH, unit=FIFF.FIFF_UNIT_MOL, coil_type=FIFF.FIFFV_COIL_FNIRS_HBO, ), hbr=dict( kind=FIFF.FIFFV_FNIRS_CH, unit=FIFF.FIFF_UNIT_MOL, coil_type=FIFF.FIFFV_COIL_FNIRS_HBR, ), csd=dict( kind=FIFF.FIFFV_EEG_CH, unit=FIFF.FIFF_UNIT_V_M2, coil_type=FIFF.FIFFV_COIL_EEG_CSD, ), temperature=dict(kind=FIFF.FIFFV_TEMPERATURE_CH, unit=FIFF.FIFF_UNIT_CEL), gsr=dict(kind=FIFF.FIFFV_GALVANIC_CH, unit=FIFF.FIFF_UNIT_S), eyegaze=dict( kind=FIFF.FIFFV_EYETRACK_CH, coil_type=FIFF.FIFFV_COIL_EYETRACK_POS ), pupil=dict( kind=FIFF.FIFFV_EYETRACK_CH, coil_type=FIFF.FIFFV_COIL_EYETRACK_PUPIL ), ) if include_defaults: coil_none = dict(coil_type=FIFF.FIFFV_COIL_NONE) unit_none = dict(unit=FIFF.FIFF_UNIT_NONE) defaults = dict( grad=dict(coil_type=FIFF.FIFFV_COIL_VV_PLANAR_T1), mag=dict(coil_type=FIFF.FIFFV_COIL_VV_MAG_T3), ref_meg=dict(coil_type=FIFF.FIFFV_COIL_VV_MAG_T3, unit=FIFF.FIFF_UNIT_T), misc=dict(**coil_none, **unit_none), # NB: overwrites UNIT_V stim=dict(unit=FIFF.FIFF_UNIT_V, **coil_none), eog=coil_none, ecg=coil_none, emg=coil_none, bio=coil_none, fnirs_od=unit_none, pupil=unit_none, eyegaze=dict(unit=FIFF.FIFF_UNIT_PX), ) for key, value in defaults.items(): base[key].update(value) return base _first_rule = { FIFF.FIFFV_MEG_CH: "meg", FIFF.FIFFV_REF_MEG_CH: "ref_meg", FIFF.FIFFV_EEG_CH: "eeg", FIFF.FIFFV_STIM_CH: "stim", FIFF.FIFFV_EOG_CH: "eog", FIFF.FIFFV_EMG_CH: "emg", FIFF.FIFFV_ECG_CH: "ecg", FIFF.FIFFV_RESP_CH: "resp", FIFF.FIFFV_MISC_CH: "misc", FIFF.FIFFV_EXCI_CH: "exci", FIFF.FIFFV_IAS_CH: "ias", FIFF.FIFFV_SYST_CH: "syst", FIFF.FIFFV_SEEG_CH: "seeg", FIFF.FIFFV_DBS_CH: "dbs", FIFF.FIFFV_BIO_CH: "bio", FIFF.FIFFV_QUAT_0: "chpi", FIFF.FIFFV_QUAT_1: "chpi", FIFF.FIFFV_QUAT_2: "chpi", FIFF.FIFFV_QUAT_3: "chpi", FIFF.FIFFV_QUAT_4: "chpi", FIFF.FIFFV_QUAT_5: "chpi", FIFF.FIFFV_QUAT_6: "chpi", FIFF.FIFFV_HPI_G: "chpi", FIFF.FIFFV_HPI_ERR: "chpi", FIFF.FIFFV_HPI_MOV: "chpi", FIFF.FIFFV_DIPOLE_WAVE: "dipole", FIFF.FIFFV_GOODNESS_FIT: "gof", FIFF.FIFFV_ECOG_CH: "ecog", FIFF.FIFFV_FNIRS_CH: "fnirs", FIFF.FIFFV_TEMPERATURE_CH: "temperature", FIFF.FIFFV_GALVANIC_CH: "gsr", FIFF.FIFFV_EYETRACK_CH: "eyetrack", } # How to reduce our categories in channel_type (originally) _second_rules = { "meg": ("unit", {FIFF.FIFF_UNIT_T_M: "grad", FIFF.FIFF_UNIT_T: "mag"}), "fnirs": ( "coil_type", { FIFF.FIFFV_COIL_FNIRS_HBO: "hbo", FIFF.FIFFV_COIL_FNIRS_HBR: "hbr", FIFF.FIFFV_COIL_FNIRS_CW_AMPLITUDE: "fnirs_cw_amplitude", FIFF.FIFFV_COIL_FNIRS_FD_AC_AMPLITUDE: "fnirs_fd_ac_amplitude", FIFF.FIFFV_COIL_FNIRS_FD_PHASE: "fnirs_fd_phase", FIFF.FIFFV_COIL_FNIRS_OD: "fnirs_od", }, ), "eeg": ( "coil_type", { FIFF.FIFFV_COIL_EEG: "eeg", FIFF.FIFFV_COIL_EEG_BIPOLAR: "eeg", FIFF.FIFFV_COIL_NONE: "eeg", # MNE-C backward compat FIFF.FIFFV_COIL_EEG_CSD: "csd", }, ), "eyetrack": ( "coil_type", { FIFF.FIFFV_COIL_EYETRACK_POS: "eyegaze", FIFF.FIFFV_COIL_EYETRACK_PUPIL: "pupil", }, ), } @fill_doc def channel_type(info, idx): """Get channel type. Parameters ---------- %(info_not_none)s idx : int Index of channel. Returns ------- type : str Type of channel. Will be one of:: {'bio', 'chpi', 'dbs', 'dipole', 'ecg', 'ecog', 'eeg', 'emg', 'eog', 'exci', 'eyetrack', 'fnirs', 'gof', 'gsr', 'ias', 'misc', 'meg', 'ref_meg', 'resp', 'seeg', 'stim', 'syst', 'temperature'} """ # This is faster than the original _channel_type_old now in test_pick.py # because it uses (at most!) two dict lookups plus one conditional # to get the channel type string. ch = info["chs"][idx] try: first_kind = _first_rule[ch["kind"]] except KeyError: raise ValueError( f'Unknown channel type ({ch["kind"]}) for channel "{ch["ch_name"]}"' ) if first_kind in _second_rules: key, second_rule = _second_rules[first_kind] first_kind = second_rule[ch[key]] return first_kind @verbose def pick_channels(ch_names, include, exclude=(), ordered=True, *, verbose=None): """Pick channels by names. Returns the indices of ``ch_names`` in ``include`` but not in ``exclude``. Parameters ---------- ch_names : list of str List of channels. include : list of str List of channels to include (if empty include all available). .. note:: This is to be treated as a set. The order of this list is not used or maintained in ``sel``. exclude : list of str List of channels to exclude (if empty do not exclude any channel). Defaults to []. %(ordered)s %(verbose)s Returns ------- sel : array of int Indices of good channels. See Also -------- pick_channels_regexp, pick_types """ if len(np.unique(ch_names)) != len(ch_names): raise RuntimeError("ch_names is not a unique list, picking is unsafe") _validate_type(ordered, bool, "ordered") _check_excludes_includes(include) _check_excludes_includes(exclude) if not isinstance(include, list): include = list(include) if len(include) == 0: include = list(ch_names) if not isinstance(exclude, list): exclude = list(exclude) sel, missing = list(), list() for name in include: if name in ch_names: if name not in exclude: sel.append(ch_names.index(name)) else: missing.append(name) if len(missing) and ordered: raise ValueError( f"Missing channels from ch_names required by include:\n{missing}" ) if not ordered: sel = np.unique(sel) return np.array(sel, int) def pick_channels_regexp(ch_names, regexp): """Pick channels using regular expression. Returns the indices of the good channels in ch_names. Parameters ---------- ch_names : list of str List of channels. regexp : str The regular expression. See python standard module for regular expressions. Returns ------- sel : array of int Indices of good channels. See Also -------- pick_channels Examples -------- >>> pick_channels_regexp(['MEG 2331', 'MEG 2332', 'MEG 2333'], 'MEG ...1') [0] >>> pick_channels_regexp(['MEG 2331', 'MEG 2332', 'MEG 2333'], 'MEG *') [0, 1, 2] """ r = re.compile(regexp) return [k for k, name in enumerate(ch_names) if r.match(name)] def _triage_meg_pick(ch, meg): """Triage an MEG pick type.""" if meg is True: return True elif ch["unit"] == FIFF.FIFF_UNIT_T_M: if meg == "grad": return True elif meg == "planar1" and ch["ch_name"].endswith("2"): return True elif meg == "planar2" and ch["ch_name"].endswith("3"): return True elif meg == "mag" and ch["unit"] == FIFF.FIFF_UNIT_T: return True return False def _triage_fnirs_pick(ch, fnirs, warned): """Triage an fNIRS pick type.""" if fnirs is True: return True elif ch["coil_type"] == FIFF.FIFFV_COIL_FNIRS_HBO and "hbo" in fnirs: return True elif ch["coil_type"] == FIFF.FIFFV_COIL_FNIRS_HBR and "hbr" in fnirs: return True elif ( ch["coil_type"] == FIFF.FIFFV_COIL_FNIRS_CW_AMPLITUDE and "fnirs_cw_amplitude" in fnirs ): return True elif ( ch["coil_type"] == FIFF.FIFFV_COIL_FNIRS_FD_AC_AMPLITUDE and "fnirs_fd_ac_amplitude" in fnirs ): return True elif ( ch["coil_type"] == FIFF.FIFFV_COIL_FNIRS_FD_PHASE and "fnirs_fd_phase" in fnirs ): return True elif ch["coil_type"] == FIFF.FIFFV_COIL_FNIRS_OD and "fnirs_od" in fnirs: return True return False def _triage_eyetrack_pick(ch, eyetrack): """Triage an eyetrack pick type.""" if eyetrack is False: return False elif eyetrack is True: return True elif ch["coil_type"] == FIFF.FIFFV_COIL_EYETRACK_PUPIL and "pupil" in eyetrack: return True elif ch["coil_type"] == FIFF.FIFFV_COIL_EYETRACK_POS and "eyegaze" in eyetrack: return True return False def _check_meg_type(meg, allow_auto=False): """Ensure a valid meg type.""" if isinstance(meg, str): allowed_types = ["grad", "mag", "planar1", "planar2"] allowed_types += ["auto"] if allow_auto else [] if meg not in allowed_types: raise ValueError( f"meg value must be one of {allowed_types} or bool, not {meg}" ) def _check_info_exclude(info, exclude): _validate_type(info, "info") info._check_consistency() if exclude is None: raise ValueError('exclude must be a list of strings or "bads"') elif exclude == "bads": exclude = info.get("bads", []) elif not isinstance(exclude, (list, tuple)): raise ValueError( 'exclude must either be "bads" or a list of strings.' " If only one channel is to be excluded, use " "[ch_name] instead of passing ch_name." ) return exclude @fill_doc def pick_types( info, meg=False, eeg=False, stim=False, eog=False, ecg=False, emg=False, ref_meg="auto", *, misc=False, resp=False, chpi=False, exci=False, ias=False, syst=False, seeg=False, dipole=False, gof=False, bio=False, ecog=False, fnirs=False, csd=False, dbs=False, temperature=False, gsr=False, eyetrack=False, include=(), exclude="bads", selection=None, ): """Pick channels by type and names. Parameters ---------- %(info_not_none)s %(pick_types_params)s Returns ------- sel : array of int Indices of good channels. """ # NOTE: Changes to this function's signature should also be changed in # PickChannelsMixin _validate_type(meg, (bool, str), "meg") exclude = _check_info_exclude(info, exclude) nchan = info["nchan"] pick = np.zeros(nchan, dtype=bool) _check_meg_type(ref_meg, allow_auto=True) _check_meg_type(meg) if isinstance(ref_meg, str) and ref_meg == "auto": ref_meg = ( "comps" in info and info["comps"] is not None and len(info["comps"]) > 0 and meg is not False ) for param in ( eeg, stim, eog, ecg, emg, misc, resp, chpi, exci, ias, syst, seeg, dipole, gof, bio, ecog, csd, dbs, temperature, gsr, ): if not isinstance(param, bool): w = ( "Parameters for all channel types (with the exception of " '"meg", "ref_meg", "fnirs", and "eyetrack") must be of type ' "bool, not {}." ) raise ValueError(w.format(type(param))) param_dict = dict( eeg=eeg, stim=stim, eog=eog, ecg=ecg, emg=emg, misc=misc, resp=resp, chpi=chpi, exci=exci, ias=ias, syst=syst, seeg=seeg, dbs=dbs, dipole=dipole, gof=gof, bio=bio, ecog=ecog, csd=csd, temperature=temperature, gsr=gsr, eyetrack=eyetrack, ) # avoid triage if possible if isinstance(meg, bool): for key in ("grad", "mag"): param_dict[key] = meg if isinstance(fnirs, bool): for key in _FNIRS_CH_TYPES_SPLIT: param_dict[key] = fnirs warned = [False] for k in range(nchan): ch_type = channel_type(info, k) try: pick[k] = param_dict[ch_type] except KeyError: # not so simple assert ( ch_type in ("grad", "mag", "ref_meg") + _FNIRS_CH_TYPES_SPLIT + _EYETRACK_CH_TYPES_SPLIT ) if ch_type in ("grad", "mag"): pick[k] = _triage_meg_pick(info["chs"][k], meg) elif ch_type == "ref_meg": pick[k] = _triage_meg_pick(info["chs"][k], ref_meg) elif ch_type in ("eyegaze", "pupil"): pick[k] = _triage_eyetrack_pick(info["chs"][k], eyetrack) else: # ch_type in ('hbo', 'hbr') pick[k] = _triage_fnirs_pick(info["chs"][k], fnirs, warned) # restrict channels to selection if provided if selection is not None: # the selection only restricts these types of channels sel_kind = [FIFF.FIFFV_MEG_CH, FIFF.FIFFV_REF_MEG_CH, FIFF.FIFFV_EEG_CH] for k in np.where(pick)[0]: if ( info["chs"][k]["kind"] in sel_kind and info["ch_names"][k] not in selection ): pick[k] = False myinclude = [info["ch_names"][k] for k in range(nchan) if pick[k]] myinclude += include if len(myinclude) == 0: sel = np.array([], int) else: sel = pick_channels(info["ch_names"], myinclude, exclude, ordered=False) return sel @verbose def pick_info(info, sel=(), copy=True, verbose=None): """Restrict an info structure to a selection of channels. Parameters ---------- %(info_not_none)s sel : list of int | None Indices of channels to include. If None, all channels are included. copy : bool If copy is False, info is modified inplace. %(verbose)s Returns ------- res : dict Info structure restricted to a selection of channels. """ # avoid circular imports from .meas_info import _bad_chans_comp info._check_consistency() info = info.copy() if copy else info if sel is None: return info elif len(sel) == 0: raise ValueError("No channels match the selection.") ch_set = set(info["ch_names"][k] for k in sel) n_unique = len(ch_set) if n_unique != len(sel): raise ValueError( f"Found {n_unique} / {len(sel)} unique names, sel is not unique" ) # make sure required the compensation channels are present if len(info.get("comps", [])) > 0: ch_names = [info["ch_names"][idx] for idx in sel] _, comps_missing = _bad_chans_comp(info, ch_names) if len(comps_missing) > 0: logger.info( f"Removing {len(info['comps'])} compensators from info because " "not all compensation channels were picked." ) with info._unlock(): info["comps"] = [] with info._unlock(): info["chs"] = [info["chs"][k] for k in sel] info._update_redundant() info["bads"] = [ch for ch in info["bads"] if ch in info["ch_names"]] if "comps" in info: comps = deepcopy(info["comps"]) for c in comps: row_idx = [ k for k, n in enumerate(c["data"]["row_names"]) if n in info["ch_names"] ] row_names = [c["data"]["row_names"][i] for i in row_idx] rowcals = c["rowcals"][row_idx] c["rowcals"] = rowcals c["data"]["nrow"] = len(row_names) c["data"]["row_names"] = row_names c["data"]["data"] = c["data"]["data"][row_idx] with info._unlock(): info["comps"] = comps if info.get("custom_ref_applied", False) and not _electrode_types(info): with info._unlock(): info["custom_ref_applied"] = FIFF.FIFFV_MNE_CUSTOM_REF_OFF # remove unused projectors if info.get("projs", False): projs = list() for p in info["projs"]: if any(ch_name in ch_set for ch_name in p["data"]["col_names"]): projs.append(p) if len(projs) != len(info["projs"]): with info._unlock(): info["projs"] = projs info._check_consistency() return info def _has_kit_refs(info, picks): """Determine if KIT ref channels are chosen. This is currently only used by make_forward_solution, which cannot run when KIT reference channels are included. """ for p in picks: if info["chs"][p]["coil_type"] == FIFF.FIFFV_COIL_KIT_REF_MAG: return True return False @verbose def pick_channels_forward( orig, include=(), exclude=(), ordered=True, copy=True, *, verbose=None ): """Pick channels from forward operator. Parameters ---------- orig : dict A forward solution. include : list of str List of channels to include (if empty, include all available). Defaults to []. exclude : list of str | 'bads' Channels to exclude (if empty, do not exclude any). Defaults to []. If 'bads', then exclude bad channels in orig. %(ordered)s copy : bool If True (default), make a copy. .. versionadded:: 0.19 %(verbose)s Returns ------- res : dict Forward solution restricted to selected channels. If include and exclude are empty it returns orig without copy. """ orig["info"]._check_consistency() if len(include) == 0 and len(exclude) == 0: return orig.copy() if copy else orig exclude = _check_excludes_includes(exclude, info=orig["info"], allow_bads=True) # Allow for possibility of channel ordering in forward solution being # different from that of the M/EEG file it is based on. sel_sol = pick_channels( orig["sol"]["row_names"], include=include, exclude=exclude, ordered=ordered ) sel_info = pick_channels( orig["info"]["ch_names"], include=include, exclude=exclude, ordered=ordered ) fwd = deepcopy(orig) if copy else orig # Check that forward solution and original data file agree on #channels if len(sel_sol) != len(sel_info): raise ValueError( "Forward solution and functional data appear to " "have different channel names, please check." ) # Do we have something? nuse = len(sel_sol) if nuse == 0: raise ValueError("Nothing remains after picking") logger.info(f" {nuse:d} out of {fwd['nchan']} channels remain after picking") # Pick the correct rows of the forward operator using sel_sol fwd["sol"]["data"] = fwd["sol"]["data"][sel_sol, :] fwd["_orig_sol"] = fwd["_orig_sol"][sel_sol, :] fwd["sol"]["nrow"] = nuse ch_names = [fwd["sol"]["row_names"][k] for k in sel_sol] fwd["nchan"] = nuse fwd["sol"]["row_names"] = ch_names # Pick the appropriate channel names from the info-dict using sel_info with fwd["info"]._unlock(): fwd["info"]["chs"] = [fwd["info"]["chs"][k] for k in sel_info] fwd["info"]._update_redundant() fwd["info"]["bads"] = [b for b in fwd["info"]["bads"] if b in ch_names] if fwd["sol_grad"] is not None: fwd["sol_grad"]["data"] = fwd["sol_grad"]["data"][sel_sol, :] fwd["_orig_sol_grad"] = fwd["_orig_sol_grad"][sel_sol, :] fwd["sol_grad"]["nrow"] = nuse fwd["sol_grad"]["row_names"] = [ fwd["sol_grad"]["row_names"][k] for k in sel_sol ] return fwd def pick_types_forward( orig, meg=False, eeg=False, ref_meg=True, seeg=False, ecog=False, dbs=False, include=(), exclude=(), ): """Pick by channel type and names from a forward operator. Parameters ---------- orig : dict A forward solution. meg : bool | str If True include MEG channels. If string it can be 'mag', 'grad', 'planar1' or 'planar2' to select only magnetometers, all gradiometers, or a specific type of gradiometer. eeg : bool If True include EEG channels. ref_meg : bool If True include CTF / 4D reference channels. seeg : bool If True include stereotactic EEG channels. ecog : bool If True include electrocorticography channels. dbs : bool If True include deep brain stimulation channels. include : list of str List of additional channels to include. If empty do not include any. exclude : list of str | str List of channels to exclude. If empty do not exclude any (default). If 'bads', exclude channels in orig['info']['bads']. Returns ------- res : dict Forward solution restricted to selected channel types. """ info = orig["info"] sel = pick_types( info, meg, eeg, ref_meg=ref_meg, seeg=seeg, ecog=ecog, dbs=dbs, include=include, exclude=exclude, ) if len(sel) == 0: raise ValueError("No valid channels found") include_ch_names = [info["ch_names"][k] for k in sel] return pick_channels_forward(orig, include_ch_names) @fill_doc def channel_indices_by_type(info, picks=None): """Get indices of channels by type. Parameters ---------- %(info_not_none)s %(picks_all)s Returns ------- idx_by_type : dict A dictionary that maps each channel type to a (possibly empty) list of channel indices. """ idx_by_type = { key: list() for key in _PICK_TYPES_KEYS if key not in ("meg", "fnirs", "eyetrack") } idx_by_type.update( mag=list(), grad=list(), hbo=list(), hbr=list(), fnirs_cw_amplitude=list(), fnirs_fd_ac_amplitude=list(), fnirs_fd_phase=list(), fnirs_od=list(), eyegaze=list(), pupil=list(), ) picks = _picks_to_idx(info, picks, none="all", exclude=(), allow_empty=True) for k in picks: ch_type = channel_type(info, k) for key in idx_by_type.keys(): if ch_type == key: idx_by_type[key].append(k) return idx_by_type @verbose def pick_channels_cov( orig, include=(), exclude="bads", ordered=True, copy=True, *, verbose=None ): """Pick channels from covariance matrix. Parameters ---------- orig : Covariance A covariance. include : list of str, (optional) List of channels to include (if empty, include all available). exclude : list of str, (optional) | 'bads' Channels to exclude (if empty, do not exclude any). Defaults to 'bads'. %(ordered)s copy : bool If True (the default), return a copy of the covariance matrix with the modified channels. If False, channels are modified in-place. .. versionadded:: 0.20.0 %(verbose)s Returns ------- res : dict Covariance solution restricted to selected channels. """ if copy: orig = orig.copy() # A little peculiarity of the cov objects is that these two fields # should not be copied over when None. if "method" in orig and orig["method"] is None: del orig["method"] if "loglik" in orig and orig["loglik"] is None: del orig["loglik"] exclude = orig["bads"] if exclude == "bads" else exclude sel = pick_channels( orig["names"], include=include, exclude=exclude, ordered=ordered ) data = orig["data"][sel][:, sel] if not orig["diag"] else orig["data"][sel] names = [orig["names"][k] for k in sel] bads = [name for name in orig["bads"] if name in orig["names"]] orig["data"] = data orig["names"] = names orig["bads"] = bads orig["dim"] = len(data) return orig def _mag_grad_dependent(info): """Determine of mag and grad should be dealt with jointly.""" # right now just uses SSS, could be computed / checked from cov # but probably overkill return any( ph.get("max_info", {}).get("sss_info", {}).get("in_order", 0) for ph in info.get("proc_history", []) ) @fill_doc def _contains_ch_type(info, ch_type): """Check whether a certain channel type is in an info object. Parameters ---------- %(info_not_none)s ch_type : str the channel type to be checked for Returns ------- has_ch_type : bool Whether the channel type is present or not. """ _validate_type(ch_type, "str", "ch_type") meg_extras = list(_MEG_CH_TYPES_SPLIT) fnirs_extras = list(_FNIRS_CH_TYPES_SPLIT) et_extras = list(_EYETRACK_CH_TYPES_SPLIT) valid_channel_types = sorted( [key for key in _PICK_TYPES_KEYS if key != "meg"] + meg_extras + fnirs_extras + et_extras ) _check_option("ch_type", ch_type, valid_channel_types) if info is None: raise ValueError( f'Cannot check for channels of type "{ch_type}" because info is None' ) return any(ch_type == channel_type(info, ii) for ii in range(info["nchan"])) @fill_doc def _picks_by_type(info, meg_combined=False, ref_meg=False, exclude="bads"): """Get data channel indices as separate list of tuples. Parameters ---------- %(info_not_none)s meg_combined : bool | 'auto' Whether to return combined picks for grad and mag. Can be 'auto' to choose based on Maxwell filtering status. ref_meg : bool If True include CTF / 4D reference channels exclude : list of str | str List of channels to exclude. If 'bads' (default), exclude channels in info['bads']. Returns ------- picks_list : list of tuples The list of tuples of picks and the type string. """ _validate_type(ref_meg, bool, "ref_meg") exclude = _check_info_exclude(info, exclude) if meg_combined == "auto": meg_combined = _mag_grad_dependent(info) picks_list = {ch_type: list() for ch_type in _DATA_CH_TYPES_SPLIT} for k in range(info["nchan"]): if info["chs"][k]["ch_name"] not in exclude: this_type = channel_type(info, k) try: picks_list[this_type].append(k) except KeyError: # This annoyance is due to differences in pick_types # and channel_type behavior if this_type == "ref_meg": ch = info["chs"][k] if _triage_meg_pick(ch, ref_meg): if ch["unit"] == FIFF.FIFF_UNIT_T: picks_list["mag"].append(k) elif ch["unit"] == FIFF.FIFF_UNIT_T_M: picks_list["grad"].append(k) else: pass # not a data channel type picks_list = [ (ch_type, np.array(picks_list[ch_type], int)) for ch_type in _DATA_CH_TYPES_SPLIT ] assert _DATA_CH_TYPES_SPLIT[:2] == ("mag", "grad") if meg_combined and len(picks_list[0][1]) and len(picks_list[1][1]): picks_list.insert( 0, ( "meg", np.unique(np.concatenate([picks_list.pop(0)[1], picks_list.pop(0)[1]])), ), ) picks_list = [p for p in picks_list if len(p[1])] return picks_list def _check_excludes_includes(chs, info=None, allow_bads=False): """Ensure that inputs to exclude/include are list-like or "bads". Parameters ---------- chs : any input, should be list, tuple, set, str The channels passed to include or exclude. allow_bads : bool Allow the user to supply "bads" as a string for auto exclusion. Returns ------- chs : list Channels to be excluded/excluded. If allow_bads, and chs=="bads", this will be the bad channels found in 'info'. """ from .meas_info import Info if not isinstance(chs, (list, tuple, set, np.ndarray)): if allow_bads is True: if not isinstance(info, Info): raise ValueError("Supply an info object if allow_bads is true") elif chs != "bads": raise ValueError('If chs is a string, it must be "bads"') else: chs = info["bads"] else: raise ValueError( 'include/exclude must be list, tuple, ndarray, or "bads". You provided ' f"type {type(chs)}." ) return chs _PICK_TYPES_DATA_DICT = dict( meg=True, eeg=True, csd=True, stim=False, eog=False, ecg=False, emg=False, misc=False, resp=False, chpi=False, exci=False, ias=False, syst=False, seeg=True, dipole=False, gof=False, bio=False, ecog=True, fnirs=True, dbs=True, temperature=False, gsr=False, eyetrack=True, ) _PICK_TYPES_KEYS = tuple(list(_PICK_TYPES_DATA_DICT) + ["ref_meg"]) _MEG_CH_TYPES_SPLIT = ("mag", "grad", "planar1", "planar2") _FNIRS_CH_TYPES_SPLIT = ( "hbo", "hbr", "fnirs_cw_amplitude", "fnirs_fd_ac_amplitude", "fnirs_fd_phase", "fnirs_od", ) _EYETRACK_CH_TYPES_SPLIT = ("eyegaze", "pupil") _DATA_CH_TYPES_ORDER_DEFAULT = ( ( "mag", "grad", "eeg", "csd", "eog", "ecg", "resp", "emg", "ref_meg", "misc", "stim", "chpi", "exci", "ias", "syst", "seeg", "bio", "ecog", "dbs", "temperature", "gsr", "gof", "dipole", ) + _FNIRS_CH_TYPES_SPLIT + _EYETRACK_CH_TYPES_SPLIT + ("whitened",) ) # Valid data types, ordered for consistency, used in viz/evoked. _VALID_CHANNEL_TYPES = ( ( "eeg", "grad", "mag", "seeg", "eog", "ecg", "resp", "emg", "dipole", "gof", "bio", "ecog", "dbs", ) + _FNIRS_CH_TYPES_SPLIT + _EYETRACK_CH_TYPES_SPLIT + ("misc", "csd") ) _DATA_CH_TYPES_SPLIT = ( "mag", "grad", "eeg", "csd", "seeg", "ecog", "dbs", ) + _FNIRS_CH_TYPES_SPLIT # Electrode types (e.g., can be average-referenced together or separately) _ELECTRODE_CH_TYPES = ("eeg", "ecog", "seeg", "dbs") def _electrode_types(info, *, exclude="bads"): return [ ch_type for ch_type in _ELECTRODE_CH_TYPES if len(pick_types(info, exclude=exclude, **{ch_type: True})) ] def _pick_data_channels(info, exclude="bads", with_ref_meg=True, with_aux=False): """Pick only data channels.""" kwargs = _PICK_TYPES_DATA_DICT if with_aux: kwargs = kwargs.copy() kwargs.update(eog=True, ecg=True, emg=True, bio=True) return pick_types(info, ref_meg=with_ref_meg, exclude=exclude, **kwargs) def _pick_data_or_ica(info, exclude=()): """Pick only data or ICA channels.""" if any(ch_name.startswith("ICA") for ch_name in info["ch_names"]): picks = pick_types(info, exclude=exclude, misc=True) else: picks = _pick_data_channels(info, exclude=exclude, with_ref_meg=True) return picks def _picks_to_idx( info, picks, none="data", exclude="bads", allow_empty=False, with_ref_meg=True, return_kind=False, picks_on="channels", ): """Convert and check pick validity. Parameters ---------- picks_on : str 'channels' (default) for error messages about selection of channels. 'components' for error messages about selection of components. """ from .meas_info import Info picked_ch_type_or_generic = False # # None -> all, data, or data_or_ica (ndarray of int) # if isinstance(info, Info): n_chan = info["nchan"] else: info = _ensure_int(info, "info", "an int or Info") n_chan = info assert n_chan >= 0 orig_picks = picks # We do some extra_repr gymnastics to avoid calling repr(orig_picks) too # soon as it can be a performance bottleneck (repr on ndarray is slow) extra_repr = "" if picks is None: if isinstance(info, int): # special wrapper for no real info picks = np.arange(n_chan) extra_repr = ", treated as range({n_chan})" else: picks = none # let _picks_str_to_idx handle it extra_repr = f'None, treated as "{none}"' # # slice # if isinstance(picks, slice): picks = np.arange(n_chan)[picks] # # -> ndarray of int (and make a copy) # picks = np.atleast_1d(picks) # this works even for picks == 'something' picks = np.array([], dtype=int) if len(picks) == 0 else picks if picks.ndim != 1: raise ValueError(f"picks must be 1D, got {picks.ndim}D") if picks.dtype.char in ("S", "U"): picks = _picks_str_to_idx( info, picks, exclude, with_ref_meg, return_kind, extra_repr, allow_empty, orig_picks, ) if return_kind: picked_ch_type_or_generic = picks[1] picks = picks[0] if picks.dtype.kind not in ["i", "u"]: extra_ch = " or list of str (names)" if picks_on == "channels" else "" msg = ( f"picks must be a list of int (indices){extra_ch}. " f"The provided data type {picks.dtype} is invalid." ) raise TypeError(msg) del extra_repr picks = picks.astype(int) # # ensure we have (optionally non-empty) ndarray of valid int # if len(picks) == 0 and not allow_empty: raise ValueError( f"No appropriate {picks_on} found for the given picks ({orig_picks!r})" ) if (picks < -n_chan).any(): raise IndexError(f"All picks must be >= {-n_chan}, got {repr(orig_picks)}") if (picks >= n_chan).any(): raise IndexError( f"All picks must be < n_{picks_on} ({n_chan}), got {repr(orig_picks)}" ) picks %= n_chan # ensure positive if return_kind: return picks, picked_ch_type_or_generic return picks def _picks_str_to_idx( info, picks, exclude, with_ref_meg, return_kind, extra_repr, allow_empty, orig_picks ): """Turn a list of str into ndarray of int.""" # special case for _picks_to_idx w/no info: shouldn't really happen if isinstance(info, int): raise ValueError( "picks as str can only be used when measurement info is available" ) # # first: check our special cases # picks_generic = list() if len(picks) == 1: if picks[0] in ("all", "data", "data_or_ica"): if picks[0] == "all": use_exclude = info["bads"] if exclude == "bads" else exclude picks_generic = pick_channels( info["ch_names"], info["ch_names"], exclude=use_exclude ) elif picks[0] == "data": picks_generic = _pick_data_channels( info, exclude=exclude, with_ref_meg=with_ref_meg ) elif picks[0] == "data_or_ica": picks_generic = _pick_data_or_ica(info, exclude=exclude) if len(picks_generic) == 0 and orig_picks is None and not allow_empty: raise ValueError( f"picks ({repr(orig_picks) + extra_repr}) yielded no channels, " "consider passing picks explicitly" ) # # second: match all to channel names # bad_names = [] picks_name = list() for pick in picks: try: picks_name.append(info["ch_names"].index(pick)) except ValueError: bad_names.append(pick) # # third: match all to types # bad_type = None picks_type = list() kwargs = dict(meg=False) meg, fnirs, eyetrack = set(), set(), set() for pick in picks: if pick in _PICK_TYPES_KEYS: kwargs[pick] = True elif pick in _MEG_CH_TYPES_SPLIT: meg |= {pick} elif pick in _FNIRS_CH_TYPES_SPLIT: fnirs |= {pick} elif pick in _EYETRACK_CH_TYPES_SPLIT: eyetrack |= {pick} else: bad_type = pick break else: # bad_type is None but this could still be empty bad_type = list(picks) # triage MEG, FNIRS, and eyetrack, which are complicated due to non-bool entries extra_picks = set() if "ref_meg" not in picks and not with_ref_meg: kwargs["ref_meg"] = False if len(meg) > 0 and not kwargs.get("meg", False): # easiest just to iterate for use_meg in meg: extra_picks |= set( pick_types(info, meg=use_meg, ref_meg=False, exclude=exclude) ) if len(fnirs) and not kwargs.get("fnirs", False): idx = 0 if len(fnirs) == 1 else slice(None) kwargs["fnirs"] = list(fnirs)[idx] if len(eyetrack) and not kwargs.get("eyetrack", False): idx = 0 if len(eyetrack) == 1 else slice(None) kwargs["eyetrack"] = list(eyetrack)[idx] # slice(None) is equivalent to all picks_type = pick_types(info, exclude=exclude, **kwargs) if len(extra_picks) > 0: picks_type = sorted(set(picks_type) | set(extra_picks)) # # finally: ensure we have exactly one usable list # all_picks = (picks_generic, picks_name, picks_type) any_found = [len(p) > 0 for p in all_picks] if sum(any_found) == 0: if not allow_empty: raise ValueError( f"picks ({repr(orig_picks) + extra_repr}) could not be interpreted as " f'channel names (no channel "{bad_names}"), channel types (no type' f' "{bad_type}" present), or a generic type (just "all" or "data")' ) picks = np.array([], int) elif sum(any_found) > 1: raise RuntimeError( "Some channel names are ambiguously equivalent to " "channel types, cannot use string-based " "picks for these" ) else: picks = np.array(all_picks[np.where(any_found)[0][0]]) picked_ch_type_or_generic = not len(picks_name) if len(bad_names) > 0 and not picked_ch_type_or_generic: raise ValueError( f"Channel(s) {bad_names} could not be picked, because " "they are not present in the info instance." ) if return_kind: return picks, picked_ch_type_or_generic return picks