Aluode/PerceptionLabPortable
0
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: