CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
meas_info.py4009 linesDownload Raw Back to _fiff
1# Authors: The MNE-Python contributors.2# License: BSD-3-Clause3# Copyright the MNE-Python contributors.4 5import contextlib6import datetime7import operator8import re9import string10from collections import Counter, OrderedDict11from collections.abc import Mapping12from copy import deepcopy13from functools import partial14from io import BytesIO15from textwrap import shorten16 17import numpy as np18 19from ..defaults import _handle_default20from ..html_templates import _get_html_template21from ..utils import (22    _check_fname,23    _check_on_missing,24    _check_option,25    _dt_to_stamp,26    _on_missing,27    _pl,28    _stamp_to_dt,29    _validate_type,30    check_fname,31    fill_doc,32    logger,33    object_diff,34    repr_html,35    verbose,36    warn,37)38from ..utils._bunch import NamedFloat, NamedInt39from ._digitization import (40    DigPoint,41    _dig_kind_ints,42    _dig_kind_proper,43    _dig_kind_rev,44    _format_dig_points,45    _get_data_as_dict_from_dig,46    _read_dig_fif,47    write_dig,48)49from .compensator import get_current_comp50from .constants import FIFF, _ch_unit_mul_named51from .ctf_comp import _read_ctf_comp, write_ctf_comp52from .open import fiff_open53from .pick import (54    _DATA_CH_TYPES_SPLIT,55    _contains_ch_type,56    _picks_to_idx,57    channel_type,58    get_channel_type_constants,59    pick_types,60)61from .proc_history import _read_proc_history, _write_proc_history62from .proj import (63    Projection,64    _normalize_proj,65    _proj_equal,66    _read_proj,67    _uniquify_projs,68    _write_proj,69)70from .tag import (71    _ch_coord_dict,72    _float_item,73    _int_item,74    _rename_list,75    _update_ch_info_named,76    find_tag,77    read_tag,78)79from .tree import dir_tree_find80from .write import (81    DATE_NONE,82    _safe_name_list,83    end_block,84    start_and_end_file,85    start_block,86    write_ch_info,87    write_coord_trans,88    write_dig_points,89    write_float,90    write_float_matrix,91    write_id,92    write_int,93    write_julian,94    write_name_list_sanitized,95    write_string,96)97 98b = bytes  # alias99 100_SCALAR_CH_KEYS = (101    "scanno",102    "logno",103    "kind",104    "range",105    "cal",106    "coil_type",107    "unit",108    "unit_mul",109    "coord_frame",110)111_ALL_CH_KEYS_SET = set(_SCALAR_CH_KEYS + ("loc", "ch_name"))112# XXX we need to require these except when doing simplify_info113_MIN_CH_KEYS_SET = set(("kind", "cal", "unit", "loc", "ch_name"))114 115 116def _get_valid_units():117    """Get valid units according to the International System of Units (SI).118 119    The International System of Units (SI, :footcite:`WikipediaSI`) is the120    default system for describing units in the Brain Imaging Data Structure121    (BIDS). For more information, see the BIDS specification122    :footcite:`BIDSdocs` and the appendix "Units" therein.123 124    References125    ----------126    .. footbibliography::127    """128    valid_prefix_names = [129        "yocto",130        "zepto",131        "atto",132        "femto",133        "pico",134        "nano",135        "micro",136        "milli",137        "centi",138        "deci",139        "deca",140        "hecto",141        "kilo",142        "mega",143        "giga",144        "tera",145        "peta",146        "exa",147        "zetta",148        "yotta",149    ]150    valid_prefix_symbols = [151        "y",152        "z",153        "a",154        "f",155        "p",156        "n",157        "µ",158        "m",159        "c",160        "d",161        "da",162        "h",163        "k",164        "M",165        "G",166        "T",167        "P",168        "E",169        "Z",170        "Y",171    ]172    valid_unit_names = [173        "metre",174        "kilogram",175        "second",176        "ampere",177        "kelvin",178        "mole",179        "candela",180        "radian",181        "steradian",182        "hertz",183        "newton",184        "pascal",185        "joule",186        "watt",187        "coulomb",188        "volt",189        "farad",190        "ohm",191        "siemens",192        "weber",193        "tesla",194        "henry",195        "degree Celsius",196        "lumen",197        "lux",198        "becquerel",199        "gray",200        "sievert",201        "katal",202    ]203    valid_unit_symbols = [204        "m",205        "kg",206        "s",207        "A",208        "K",209        "mol",210        "cd",211        "rad",212        "sr",213        "Hz",214        "N",215        "Pa",216        "J",217        "W",218        "C",219        "V",220        "F",221        "Ω",222        "S",223        "Wb",224        "T",225        "H",226        "°C",227        "lm",228        "lx",229        "Bq",230        "Gy",231        "Sv",232        "kat",233    ]234 235    # Valid units are all possible combinations of either prefix name or prefix236    # symbol together with either unit name or unit symbol. E.g., nV for237    # nanovolt238    valid_units = []239    valid_units += [240        "".join([prefix, unit])241        for prefix in valid_prefix_names242        for unit in valid_unit_names243    ]244    valid_units += [245        "".join([prefix, unit])246        for prefix in valid_prefix_names247        for unit in valid_unit_symbols248    ]249    valid_units += [250        "".join([prefix, unit])251        for prefix in valid_prefix_symbols252        for unit in valid_unit_names253    ]254    valid_units += [255        "".join([prefix, unit])256        for prefix in valid_prefix_symbols257        for unit in valid_unit_symbols258    ]259 260    # units are also valid without a prefix261    valid_units += valid_unit_names262    valid_units += valid_unit_symbols263 264    # we also accept "n/a" as a unit, which is the default missing value in265    # BIDS266    valid_units += ["n/a"]267 268    return tuple(valid_units)269 270 271@verbose272def _unique_channel_names(ch_names, max_length=None, verbose=None):273    """Ensure unique channel names."""274    suffixes = tuple(string.ascii_lowercase)275    if max_length is not None:276        ch_names[:] = [name[:max_length] for name in ch_names]277    unique_ids = np.unique(ch_names, return_index=True)[1]278    if len(unique_ids) != len(ch_names):279        dups = {ch_names[x] for x in np.setdiff1d(range(len(ch_names)), unique_ids)}280        warn(281            "Channel names are not unique, found duplicates for: "282            f"{dups}. Applying running numbers for duplicates."283        )284        for ch_stem in dups:285            overlaps = np.where(np.array(ch_names) == ch_stem)[0]286            # We need an extra character since we append '-'.287            # np.ceil(...) is the maximum number of appended digits.288            if max_length is not None:289                n_keep = max_length - 1 - int(np.ceil(np.log10(len(overlaps))))290            else:291                n_keep = np.inf292            n_keep = min(len(ch_stem), n_keep)293            ch_stem = ch_stem[:n_keep]294            for idx, ch_idx in enumerate(overlaps):295                # try idx first, then loop through lower case chars296                for suffix in (idx,) + suffixes:297                    ch_name = ch_stem + f"-{suffix}"298                    if ch_name not in ch_names:299                        break300                if ch_name not in ch_names:301                    ch_names[ch_idx] = ch_name302                else:303                    raise ValueError(304                        "Adding a single alphanumeric for a "305                        "duplicate resulted in another "306                        f"duplicate name {ch_name}"307                    )308    return ch_names309 310 311# %% Mixin classes312 313 314class MontageMixin:315    """Mixin for Montage getting and setting."""316 317    @fill_doc318    def get_montage(self):319        """Get a DigMontage from instance.320 321        Returns322        -------323        montage : None | DigMontage324            A copy of the channel positions, if available, otherwise ``None``.325        """326        from ..channels.montage import make_dig_montage327        from ..transforms import _frame_to_str328 329        info = self if isinstance(self, Info) else self.info330        if info["dig"] is None:331            return None332        # obtain coord_frame, and landmark coords333        # (nasion, lpa, rpa, hsp, hpi) from DigPoints334        montage_bunch = _get_data_as_dict_from_dig(info["dig"])335        coord_frame = _frame_to_str.get(montage_bunch.coord_frame)336 337        # get the channel names and chs data structure338        ch_names, chs = info["ch_names"], info["chs"]339        picks = pick_types(340            info,341            meg=False,342            eeg=True,343            seeg=True,344            ecog=True,345            dbs=True,346            fnirs=True,347            exclude=[],348        )349 350        # channel positions from dig do not match ch_names one to one,351        # so use loc[:3] instead352        ch_pos = {ch_names[ii]: chs[ii]["loc"][:3] for ii in picks}353 354        # fNIRS uses multiple channels for the same sensors, we use355        # a private function to format these for dig montage.356        fnirs_picks = pick_types(info, fnirs=True, exclude=[])357        if len(ch_pos) == len(fnirs_picks):358            ch_pos = _get_fnirs_ch_pos(info)359        elif len(fnirs_picks) > 0:360            raise ValueError(361                "MNE does not support getting the montage "362                "for a mix of fNIRS and other data types. "363                "Please raise a GitHub issue if you "364                "require this feature."365            )366 367        # create montage368        montage = make_dig_montage(369            ch_pos=ch_pos,370            coord_frame=coord_frame,371            nasion=montage_bunch.nasion,372            lpa=montage_bunch.lpa,373            rpa=montage_bunch.rpa,374            hsp=montage_bunch.hsp,375            hpi=montage_bunch.hpi,376        )377        return montage378 379    @verbose380    def set_montage(381        self,382        montage,383        match_case=True,384        match_alias=False,385        on_missing="raise",386        verbose=None,387    ):388        """Set %(montage_types)s channel positions and digitization points.389 390        Parameters391        ----------392        %(montage)s393        %(match_case)s394        %(match_alias)s395        %(on_missing_montage)s396        %(verbose)s397 398        Returns399        -------400        inst : instance of Raw | Epochs | Evoked401            The instance, modified in-place.402 403        See Also404        --------405        mne.channels.make_standard_montage406        mne.channels.make_dig_montage407        mne.channels.read_custom_montage408 409        Notes410        -----411        .. warning::412            Only %(montage_types)s channels can have their positions set using413            a montage. Other channel types (e.g., MEG channels) should have414            their positions defined properly using their data reading415            functions.416        .. warning::417            Applying a montage will only set locations of channels that exist418            at the time it is applied. This means when419            :ref:`re-referencing <tut-set-eeg-ref>`420            make sure to apply the montage only after calling421            :func:`mne.add_reference_channels`422        """423        # How to set up a montage to old named fif file (walk through example)424        # https://gist.github.com/massich/f6a9f4799f1fbeb8f5e8f8bc7b07d3df425 426        from ..channels.montage import _set_montage427 428        info = self if isinstance(self, Info) else self.info429        _set_montage(info, montage, match_case, match_alias, on_missing)430        return self431 432 433channel_type_constants = get_channel_type_constants(include_defaults=True)434_human2fiff = {435    k: v.get("kind", FIFF.FIFFV_COIL_NONE) for k, v in channel_type_constants.items()436}437_human2unit = {438    k: v.get("unit", FIFF.FIFF_UNIT_NONE) for k, v in channel_type_constants.items()439}440_unit2human = {441    FIFF.FIFF_UNIT_V: "V",442    FIFF.FIFF_UNIT_T: "T",443    FIFF.FIFF_UNIT_T_M: "T/m",444    FIFF.FIFF_UNIT_MOL: "M",445    FIFF.FIFF_UNIT_NONE: "NA",446    FIFF.FIFF_UNIT_CEL: "C",447    FIFF.FIFF_UNIT_S: "S",448    FIFF.FIFF_UNIT_PX: "px",449}450 451 452def _check_set(ch, projs, ch_type):453    """Ensure type change is compatible with projectors."""454    new_kind = _human2fiff[ch_type]455    if ch["kind"] != new_kind:456        for proj in projs:457            if ch["ch_name"] in proj["data"]["col_names"]:458                raise RuntimeError(459                    f"Cannot change channel type for channel {ch['ch_name']} in "460                    f'projector "{proj["desc"]}"'461                )462    ch["kind"] = new_kind463 464 465class SetChannelsMixin(MontageMixin):466    """Mixin class for Raw, Evoked, Epochs."""467 468    def _get_channel_positions(self, picks=None):469        """Get channel locations from info.470 471        Parameters472        ----------473        picks : str | list | slice | None474            None gets good data indices.475 476        Notes477        -----478        .. versionadded:: 0.9.0479        """480        info = self if isinstance(self, Info) else self.info481        picks = _picks_to_idx(info, picks)482        chs = info["chs"]483        pos = np.array([chs[k]["loc"][:3] for k in picks])484        n_zero = np.sum(np.sum(np.abs(pos), axis=1) == 0)485        if n_zero > 1:  # XXX some systems have origin (0, 0, 0)486            raise ValueError(487                f"Could not extract channel positions for {n_zero} channels"488            )489        return pos490 491    def _set_channel_positions(self, pos, names):492        """Update channel locations in info.493 494        Parameters495        ----------496        pos : array-like | np.ndarray, shape (n_points, 3)497            The channel positions to be set.498        names : list of str499            The names of the channels to be set.500 501        Notes502        -----503        .. versionadded:: 0.9.0504        """505        info = self if isinstance(self, Info) else self.info506        if len(pos) != len(names):507            raise ValueError(508                "Number of channel positions not equal to the number of names given."509            )510        pos = np.asarray(pos, dtype=np.float64)511        if pos.shape[-1] != 3 or pos.ndim != 2:512            msg = (513                f"Channel positions must have the shape (n_points, 3) not {pos.shape}."514            )515            raise ValueError(msg)516        for name, p in zip(names, pos):517            if name in self.ch_names:518                idx = self.ch_names.index(name)519                info["chs"][idx]["loc"][:3] = p520            else:521                msg = f"{name} was not found in the info. Cannot be updated."522                raise ValueError(msg)523 524    @verbose525    def set_channel_types(self, mapping, *, on_unit_change="warn", verbose=None):526        """Specify the sensor types of channels.527 528        Parameters529        ----------530        mapping : dict531            A dictionary mapping channel names to sensor types, e.g.,532            ``{'EEG061': 'eog'}``.533        on_unit_change : ``'raise'`` | ``'warn'`` | ``'ignore'``534            What to do if the measurement unit of a channel is changed535            automatically to match the new sensor type.536 537            .. versionadded:: 1.4538        %(verbose)s539 540        Returns541        -------542        inst : instance of Raw | Epochs | Evoked543            The instance (modified in place).544 545            .. versionchanged:: 0.20546               Return the instance.547 548        Notes549        -----550        The following :term:`sensor types` are accepted:551 552            bio, chpi, csd, dbs, dipole, ecg, ecog, eeg, emg, eog, exci,553            eyegaze, fnirs_cw_amplitude, fnirs_fd_ac_amplitude, fnirs_fd_phase,554            fnirs_od, gof, gsr, hbo, hbr, ias, misc, pupil, ref_meg, resp,555            seeg, stim, syst, temperature.556 557        When working with eye-tracking data, see558        :func:`mne.preprocessing.eyetracking.set_channel_types_eyetrack`.559 560        .. versionadded:: 0.9.0561        """562        info = self if isinstance(self, Info) else self.info563        ch_names = info["ch_names"]564 565        # first check and assemble clean mappings of index and name566        unit_changes = dict()567        for ch_name, ch_type in mapping.items():568            if ch_name not in ch_names:569                raise ValueError(570                    f"This channel name ({ch_name}) doesn't exist in info."571                )572 573            c_ind = ch_names.index(ch_name)574            if ch_type not in _human2fiff:575                raise ValueError(576                    f"This function cannot change to this channel type: {ch_type}. "577                    "Accepted channel types are "578                    f"{', '.join(sorted(_human2unit.keys()))}."579                )580            # Set sensor type581            _check_set(info["chs"][c_ind], info["projs"], ch_type)582            unit_old = info["chs"][c_ind]["unit"]583            unit_new = _human2unit[ch_type]584            if unit_old not in _unit2human:585                raise ValueError(586                    f"Channel '{ch_name}' has unknown unit ({unit_old}). Please fix the"587                    " measurement info of your data."588                )589            if unit_old != _human2unit[ch_type]:590                this_change = (_unit2human[unit_old], _unit2human[unit_new])591                if this_change not in unit_changes:592                    unit_changes[this_change] = list()593                unit_changes[this_change].append(ch_name)594                # reset unit multiplication factor since the unit has now changed595                info["chs"][c_ind]["unit_mul"] = _ch_unit_mul_named[0]596            info["chs"][c_ind]["unit"] = _human2unit[ch_type]597            if ch_type in ["eeg", "seeg", "ecog", "dbs"]:598                coil_type = FIFF.FIFFV_COIL_EEG599            elif ch_type == "hbo":600                coil_type = FIFF.FIFFV_COIL_FNIRS_HBO601            elif ch_type == "hbr":602                coil_type = FIFF.FIFFV_COIL_FNIRS_HBR603            elif ch_type == "fnirs_cw_amplitude":604                coil_type = FIFF.FIFFV_COIL_FNIRS_CW_AMPLITUDE605            elif ch_type == "fnirs_fd_ac_amplitude":606                coil_type = FIFF.FIFFV_COIL_FNIRS_FD_AC_AMPLITUDE607            elif ch_type == "fnirs_fd_phase":608                coil_type = FIFF.FIFFV_COIL_FNIRS_FD_PHASE609            elif ch_type == "fnirs_od":610                coil_type = FIFF.FIFFV_COIL_FNIRS_OD611            elif ch_type == "eyetrack_pos":612                coil_type = FIFF.FIFFV_COIL_EYETRACK_POS613            elif ch_type == "eyetrack_pupil":614                coil_type = FIFF.FIFFV_COIL_EYETRACK_PUPIL615            else:616                coil_type = FIFF.FIFFV_COIL_NONE617            info["chs"][c_ind]["coil_type"] = coil_type618 619        msg = "The unit for channel(s) {0} has changed from {1} to {2}."620        for this_change, names in unit_changes.items():621            _on_missing(622                on_missing=on_unit_change,623                msg=msg.format(", ".join(sorted(names)), *this_change),624                name="on_unit_change",625            )626 627        return self628 629    @verbose630    def rename_channels(631        self, mapping, allow_duplicates=False, *, on_missing="raise", verbose=None632    ):633        """Rename channels.634 635        Parameters636        ----------637        %(mapping_rename_channels_duplicates)s638        %(on_missing_ch_names)s639 640            .. versionadded:: 1.11.0641        %(verbose)s642 643        Returns644        -------645        inst : instance of Raw | Epochs | Evoked646            The instance (modified in place).647 648            .. versionchanged:: 0.20649               Return the instance.650 651        Notes652        -----653        .. versionadded:: 0.9.0654        """655        from ..channels.channels import rename_channels656        from ..io import BaseRaw657 658        info = self if isinstance(self, Info) else self.info659 660        ch_names_orig = list(info["ch_names"])661        rename_channels(info, mapping, allow_duplicates, on_missing=on_missing)662 663        # Update self._orig_units for Raw664        if isinstance(self, BaseRaw):665            # whatever mapping was provided, now we can just use a dict666            mapping = dict(zip(ch_names_orig, info["ch_names"]))667            for old_name, new_name in mapping.items():668                if old_name in self._orig_units:669                    self._orig_units[new_name] = self._orig_units.pop(old_name)670            ch_names = self.annotations.ch_names671            for ci, ch in enumerate(ch_names):672                ch_names[ci] = tuple(mapping.get(name, name) for name in ch)673 674        return self675 676    @verbose677    def plot_sensors(678        self,679        kind="topomap",680        ch_type=None,681        title=None,682        show_names=False,683        ch_groups=None,684        to_sphere=True,685        axes=None,686        block=False,687        show=True,688        sphere=None,689        *,690        verbose=None,691    ):692        """Plot sensor positions.693 694        Parameters695        ----------696        kind : str697            Whether to plot the sensors as 3d, topomap or as an interactive698            sensor selection dialog. Available options 'topomap', '3d',699            'select'. If 'select', a set of channels can be selected700            interactively by using lasso selector or clicking while holding701            control key. The selected channels are returned along with the702            figure instance. Defaults to 'topomap'.703        ch_type : None | str704            The channel type to plot. Available options ``'mag'``, ``'grad'``,705            ``'eeg'``, ``'seeg'``, ``'dbs'``, ``'ecog'``, ``'all'``. If ``'all'``, all706            the available mag, grad, eeg, seeg, dbs, and ecog channels are plotted. If707            None (default), then channels are chosen in the order given above.708        title : str | None709            Title for the figure. If None (default), equals to ``'Sensor710            positions (%%s)' %% ch_type``.711        show_names : bool | array of str712            Whether to display all channel names. If an array, only the channel713            names in the array are shown. Defaults to False.714        ch_groups : 'position' | array of shape (n_ch_groups, n_picks) | None715            Channel groups for coloring the sensors. If None (default), default716            coloring scheme is used. If 'position', the sensors are divided717            into 8 regions. See ``order`` kwarg of :func:`mne.viz.plot_raw`. If718            array, the channels are divided by picks given in the array.719 720            .. versionadded:: 0.13.0721        to_sphere : bool722            Whether to project the 3d locations to a sphere. When False, the723            sensor array appears similar as to looking downwards straight above724            the subject's head. Has no effect when kind='3d'. Defaults to True.725 726            .. versionadded:: 0.14.0727        axes : instance of Axes | instance of Axes3D | None728            Axes to draw the sensors to. If ``kind='3d'``, axes must be an729            instance of Axes3D. If None (default), a new axes will be created.730 731            .. versionadded:: 0.13.0732        block : bool733            Whether to halt program execution until the figure is closed.734            Defaults to False.735 736            .. versionadded:: 0.13.0737        show : bool738            Show figure if True. Defaults to True.739        %(sphere_topomap_auto)s740        %(verbose)s741 742        Returns743        -------744        fig : instance of Figure745            Figure containing the sensor topography.746        selection : list747            A list of selected channels. Only returned if ``kind=='select'``.748 749        See Also750        --------751        mne.viz.plot_layout752 753        Notes754        -----755        This function plots the sensor locations from the info structure using756        matplotlib. For drawing the sensors using PyVista see757        :func:`mne.viz.plot_alignment`.758 759        .. versionadded:: 0.12.0760        """761        from ..viz.utils import plot_sensors762 763        return plot_sensors(764            self if isinstance(self, Info) else self.info,765            kind=kind,766            ch_type=ch_type,767            title=title,768            show_names=show_names,769            ch_groups=ch_groups,770            to_sphere=to_sphere,771            axes=axes,772            block=block,773            show=show,774            sphere=sphere,775            verbose=verbose,776        )777 778    @verbose779    def anonymize(self, daysback=None, keep_his=False, verbose=None):780        """Anonymize measurement information in place.781 782        Parameters783        ----------784        %(daysback_anonymize_info)s785        %(keep_his_anonymize_info)s786        %(verbose)s787 788        Returns789        -------790        inst : instance of Raw | Epochs | Evoked791            The modified instance.792 793        Notes794        -----795        %(anonymize_info_notes)s796 797        .. versionadded:: 0.13.0798        """799        info = self if isinstance(self, Info) else self.info800        anonymize_info(info, daysback=daysback, keep_his=keep_his, verbose=verbose)801        self.set_meas_date(info["meas_date"])  # unify annot update802        return self803 804    def set_meas_date(self, meas_date):805        """Set the measurement start date.806 807        Parameters808        ----------809        meas_date : datetime | float | tuple | None810            The new measurement date.811            If datetime object, it must be timezone-aware and in UTC.812            A tuple of (seconds, microseconds) or float (alias for813            ``(meas_date, 0)``) can also be passed and a datetime814            object will be automatically created. If None, will remove815            the time reference.816 817        Returns818        -------819        inst : instance of Raw | Epochs | Evoked820            The modified raw instance. Operates in place.821 822        See Also823        --------824        mne.io.Raw.anonymize825 826        Notes827        -----828        If you want to remove all time references in the file, call829        :func:`mne.io.anonymize_info(inst.info) <mne.io.anonymize_info>`830        after calling ``inst.set_meas_date(None)``.831 832        .. versionadded:: 0.20833        """834        from ..annotations import _handle_meas_date835 836        info = self if isinstance(self, Info) else self.info837 838        meas_date = _handle_meas_date(meas_date)839        with info._unlock():840            info["meas_date"] = meas_date841 842        # clear file_id and meas_id if needed843        if meas_date is None:844            for key in ("file_id", "meas_id"):845                value = info.get(key)846                if value is not None:847                    assert "msecs" not in value848                    value["secs"] = DATE_NONE[0]849                    value["usecs"] = DATE_NONE[1]850                    # The following copy is needed for a test CTF dataset851                    # otherwise value['machid'][:] = 0 would suffice852                    _tmp = value["machid"].copy()853                    _tmp[:] = 0854                    value["machid"] = _tmp855 856        if hasattr(self, "annotations"):857            self.annotations._orig_time = meas_date858        return self859 860 861class ContainsMixin:862    """Mixin class for Raw, Evoked, Epochs and Info."""863 864    def __contains__(self, ch_type):865        """Check channel type membership.866 867        Parameters868        ----------869        ch_type : str870            Channel type to check for. Can be e.g. ``'meg'``, ``'eeg'``,871            ``'stim'``, etc.872 873        Returns874        -------875        in : bool876            Whether or not the instance contains the given channel type.877 878        Examples879        --------880        Channel type membership can be tested as::881 882            >>> 'meg' in inst  # doctest: +SKIP883            True884            >>> 'seeg' in inst  # doctest: +SKIP885            False886 887        """888        # this method is not supported by Info object. An Info object inherits from a889        # dictionary and the 'key' in Info call is present all across MNE codebase, e.g.890        # to check for the presence of a key:891        # >>> 'bads' in info892        if ch_type == "meg":893            has_ch_type = _contains_ch_type(self.info, "mag") or _contains_ch_type(894                self.info, "grad"895            )896        else:897            has_ch_type = _contains_ch_type(self.info, ch_type)898        return has_ch_type899 900    @property901    def compensation_grade(self):902        """The current gradient compensation grade."""903        info = self if isinstance(self, Info) else self.info904        return get_current_comp(info)905 906    @fill_doc907    def get_channel_types(self, picks=None, unique=False, only_data_chs=False):908        """Get a list of channel type for each channel.909 910        Parameters911        ----------912        %(picks_all)s913        unique : bool914            Whether to return only unique channel types. Default is ``False``.915        only_data_chs : bool916            Whether to ignore non-data channels. Default is ``False``.917 918        Returns919        -------920        channel_types : list921            The channel types.922        """923        info = self if isinstance(self, Info) else self.info924        none = "data" if only_data_chs else "all"925        picks = _picks_to_idx(info, picks, none, (), allow_empty=False)926        ch_types = [channel_type(info, pick) for pick in picks]927        if only_data_chs:928            ch_types = [929                ch_type for ch_type in ch_types if ch_type in _DATA_CH_TYPES_SPLIT930            ]931        if unique:932            # set does not preserve order but dict does, so let's just use it933            ch_types = list({k: k for k in ch_types}.keys())934        return ch_types935 936 937# %% ValidatedDict class938 939 940class ValidatedDict(dict):941    _attributes = {}  # subclasses should set this to validated attributes942 943    def __init__(self, *args, **kwargs):944        self._unlocked = True945        super().__init__(*args, **kwargs)946        self._unlocked = False947 948    def __getstate__(self):949        """Get state (for pickling)."""950        return {"_unlocked": self._unlocked}951 952    def __setstate__(self, state):953        """Set state (for pickling)."""954        self._unlocked = state["_unlocked"]955 956    def __setitem__(self, key, val):957        """Attribute setter."""958        # During unpickling, the _unlocked attribute has not been set, so959        # let __setstate__ do it later and act unlocked now960        unlocked = getattr(self, "_unlocked", True)961        if key in self._attributes:962            if isinstance(self._attributes[key], str):963                if not unlocked:964                    raise RuntimeError(self._attributes[key])965            else:966                val = self._attributes[key](967                    val, info=self968                )  # attribute checker function969        else:970            class_name = self.__class__.__name__971            extra = ""972            if "temp" in self._attributes:973                var_name = _camel_to_snake(class_name)974                extra = (975                    f"You can set {var_name}['temp'] to store temporary objects in "976                    f"{class_name} instances, but these will not survive an I/O "977                    "round-trip."978                )979            raise RuntimeError(980                f"{class_name} does not support directly setting the key {repr(key)}. "981                + extra982            )983        super().__setitem__(key, val)984 985    def update(self, other=None, **kwargs):986        """Update method using __setitem__()."""987        iterable = other.items() if isinstance(other, Mapping) else other988        if other is not None:989            for key, val in iterable:990                self[key] = val991        for key, val in kwargs.items():992            self[key] = val993 994    def copy(self):995        """Copy the instance.996 997        Returns998        -------999        info : instance of Info1000            The copied info.1001        """1002        return deepcopy(self)1003 1004    def __repr__(self):1005        """Return a string representation."""1006        mapping = ", ".join(f"{key}: {val}" for key, val in self.items())1007        return f"<{_camel_to_snake(self.__class__.__name__)} | {mapping}>"1008 1009 1010# %% Subject info1011 1012 1013def _check_types(x, *, info, name, types, cast=None):1014    _validate_type(x, types, name)1015    if cast is not None and x is not None:1016        x = cast(x)1017    return x1018 1019 1020def _check_bday(birthday_input, *, info):1021    date = _check_types(1022        birthday_input,1023        info=info,1024        name='subject_info["birthday"]',1025        types=(datetime.date, None),1026    )1027    # test if we have a pd.Timestamp1028    if hasattr(date, "date"):1029        date = date.date()1030    return date1031 1032 1033class SubjectInfo(ValidatedDict):1034    _attributes = {1035        "id": partial(_check_types, name='subject_info["id"]', types=int),1036        "his_id": partial(_check_types, name='subject_info["his_id"]', types=str),1037        "last_name": partial(_check_types, name='subject_info["last_name"]', types=str),1038        "first_name": partial(1039            _check_types, name='subject_info["first_name"]', types=str1040        ),1041        "middle_name": partial(1042            _check_types, name='subject_info["middle_name"]', types=str1043        ),1044        "birthday": partial(_check_bday),1045        "sex": partial(_check_types, name='subject_info["sex"]', types=int),1046        "hand": partial(_check_types, name='subject_info["hand"]', types=int),1047        "weight": partial(1048            _check_types, name='subject_info["weight"]', types="numeric", cast=float1049        ),1050        "height": partial(1051            _check_types, name='subject_info["height"]', types="numeric", cast=float1052        ),1053    }1054 1055    def __init__(self, initial):1056        _validate_type(initial, dict, "subject_info")1057        super().__init__()1058        for key, val in initial.items():1059            self[key] = val1060 1061 1062class HeliumInfo(ValidatedDict):1063    _attributes = {1064        "he_level_raw": partial(1065            _check_types,1066            name='helium_info["he_level_raw"]',1067            types="numeric",1068            cast=float,1069        ),1070        "helium_level": partial(1071            _check_types,1072            name='helium_info["helium_level"]',1073            types="numeric",1074            cast=float,1075        ),1076        "orig_file_guid": partial(1077            _check_types, name='helium_info["orig_file_guid"]', types=str1078        ),1079        "meas_date": partial(1080            _check_types,1081            name='helium_info["meas_date"]',1082            types=(datetime.datetime, None),1083        ),1084    }1085 1086    def __init__(self, initial):1087        _validate_type(initial, dict, "helium_info")1088        super().__init__()1089        for key, val in initial.items():1090            self[key] = val1091 1092 1093# %% Info class and helpers1094 1095 1096def _format_trans(obj, key):1097    from ..transforms import Transform1098 1099    try:1100        t = obj[key]1101    except KeyError:1102        pass1103    else:1104        if t is not None:1105            obj[key] = Transform(t["from"], t["to"], t["trans"])1106 1107 1108def _check_ch_keys(ch, ci, name='info["chs"]', check_min=True):1109    ch_keys = set(ch)1110    bad = sorted(ch_keys.difference(_ALL_CH_KEYS_SET))1111    if bad:1112        raise KeyError(f"key{_pl(bad)} errantly present for {name}[{ci}]: {bad}")1113    if check_min:1114        bad = sorted(_MIN_CH_KEYS_SET.difference(ch_keys))1115        if bad:1116            raise KeyError(1117                f"key{_pl(bad)} missing for {name}[{ci}]: {bad}",1118            )1119 1120 1121def _check_bads_info_compat(bads, info):1122    _validate_type(bads, list, "bads")1123    if not len(bads):1124        return  # e.g. in empty_info1125    for bi, bad in enumerate(bads):1126        _validate_type(bad, str, f"bads[{bi}]")1127    if "ch_names" not in info:  # somewhere in init, or deepcopy, or _empty_info, etc.1128        return1129    missing = [bad for bad in bads if bad not in info["ch_names"]]1130    if len(missing) > 0:1131        raise ValueError(f"bad channel(s) {missing} marked do not exist in info")1132 1133 1134class MNEBadsList(list):1135    """Subclass of bads that checks inplace operations."""1136 1137    def __init__(self, *, bads, info):1138        _check_bads_info_compat(bads, info)1139        self._mne_info = info1140        super().__init__(bads)1141 1142    def extend(self, iterable):1143        if not isinstance(iterable, list):1144            iterable = list(iterable)1145        # can happen during pickling1146        try:1147            info = self._mne_info1148        except AttributeError:1149            pass  # can happen during pickling1150        else:1151            _check_bads_info_compat(iterable, info)1152        return super().extend(iterable)1153 1154    def append(self, x):1155        return self.extend([x])1156 1157    def __iadd__(self, x):1158        self.extend(x)1159        return self1160 1161 1162# As options are added here, test_meas_info.py:test_info_bad should be updated1163def _check_bads(bads, *, info):1164    return MNEBadsList(bads=bads, info=info)1165 1166 1167def _check_dev_head_t(dev_head_t, *, info):1168    from ..transforms import Transform, _ensure_trans1169 1170    _validate_type(dev_head_t, (Transform, None), "info['dev_head_t']")1171    if dev_head_t is not None:1172        dev_head_t = _ensure_trans(dev_head_t, "meg", "head")1173    return dev_head_t1174 1175 1176def _restore_mne_types(info):1177    """Restore MNE-specific types after unpickling/deserialization.1178 1179    This function handles the restoration of MNE-specific object types1180    that need to be reconstructed from their serialized representations.1181    These correspond to the "cast" entries in Info._attributes: bads,1182    dev_head_t, dig, helium_info, line_freq, proj_id, projs, and1183    subject_info. However, this function is specifically for types that1184    need restoration because h5io and other serialization formats cast1185    them to native Python types (e.g., MNEBadsList -> list, Projection1186    -> dict, DigPoint -> dict).1187 1188    This function should be called in Info.__init__ and Info.__setstate__.1189    If new MNE-specific types are added to Info._attributes, they1190    should be handled here if they need type restoration after1191    deserialization.1192 1193    Parameters1194    ----------1195    info : Info1196        The Info object whose types need to be restored. Modified in-place.1197 1198    Notes1199    -----1200    This function restores:

Showing the first 1,200 of 4009 lines. Download the file for the rest.