From 7aa3a27631b16fc9c16852c720e7243d876fe104 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Sat, 27 Aug 2022 00:44:52 +0100 Subject: [PATCH] ENH: Add temperature and galvanic (#11090) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ENH: Add temperature and galvanic * FIX: Use correct name * FIX: Flake * FIX: Rename * DOC: Sp * Update doc/_includes/channel_types.rst Co-authored-by: Richard Höchenberger Co-authored-by: Richard Höchenberger --- doc/_includes/channel_types.rst | 4 ++++ doc/changes/latest.inc | 1 + mne/channels/channels.py | 12 +++++++----- mne/defaults.py | 16 ++++++++++------ mne/io/constants.py | 8 ++++++-- mne/io/pick.py | 29 ++++++++++++++++++++--------- mne/io/tests/test_constants.py | 2 +- mne/utils/docs.py | 4 ++++ mne/viz/raw.py | 2 +- 9 files changed, 54 insertions(+), 24 deletions(-) diff --git a/doc/_includes/channel_types.rst b/doc/_includes/channel_types.rst index 6c0adc18c3d..647dab25ba4 100644 --- a/doc/_includes/channel_types.rst +++ b/doc/_includes/channel_types.rst @@ -65,4 +65,8 @@ ias Internal Active Shielding data syst System status channel information (Triux systems only) + +temperature Temperature Degrees Celsius + +gsr Galvanic skin response Siemens ============= ========================================= ================= diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index 05b238a21b5..884aadf9cfa 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -33,6 +33,7 @@ Enhancements - Add ``starting_affine`` keyword argument to :func:`mne.transforms.compute_volume_registration` to initialize an alignment with an affine (:gh:`11020` by `Alex Rockhill`_) - The ``trans`` parameter in :func:`mne.make_field_map` now accepts a :class:`~pathlib.Path` object, and uses standardised loading logic (:gh:`10784` by :newcontrib:`Andrew Quinn`) - Add HTML representation for `~mne.Evoked` in Jupyter Notebooks (:gh:`11075` by `Valerii Chirkov`_ and `Andrew Quinn`_) +- Add support for ``temperature`` and ``gsr`` (galvanic skin response, i.e., electrodermal activity) channel types (:gh:`11090` by `Eric Larson`_) - Allow :func:`mne.beamformer.make_dics` to take ``pick_ori='vector'`` to compute vector source estimates (:gh:`19080` by `Alex Rockhill`_) - Add ``on_missing`` functionality to all of our classes that have a ``drop_channels`` method, to control what happens when channel names are not in the object (:gh:`11077` by `Andrew Quinn`_) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index d1261ee4b9e..f2f22fadc7e 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -328,7 +328,7 @@ def set_channel_types(self, mapping, verbose=None): ecg, eeg, emg, eog, exci, ias, misc, resp, seeg, dbs, stim, syst, ecog, hbo, hbr, fnirs_cw_amplitude, fnirs_fd_ac_amplitude, - fnirs_fd_phase, fnirs_od + fnirs_fd_phase, fnirs_od, temperature, gsr .. versionadded:: 0.9.0 """ @@ -590,11 +590,12 @@ class UpdateChannelsMixin(object): @verbose def pick_types(self, meg=False, eeg=False, stim=False, eog=False, - ecg=False, emg=False, ref_meg='auto', misc=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, include=(), - exclude='bads', selection=None, verbose=None): + ecog=False, fnirs=False, csd=False, dbs=False, + temperature=False, gsr=False, + include=(), exclude='bads', selection=None, verbose=None): """Pick some channels by type and names. Parameters @@ -620,7 +621,8 @@ def pick_types(self, meg=False, eeg=False, stim=False, eog=False, ref_meg=ref_meg, misc=misc, resp=resp, chpi=chpi, exci=exci, ias=ias, syst=syst, seeg=seeg, dipole=dipole, gof=gof, bio=bio, ecog=ecog, fnirs=fnirs, csd=csd, dbs=dbs, include=include, - exclude=exclude, selection=selection) + exclude=exclude, selection=selection, temperature=temperature, + gsr=gsr) self._pick_drop_channels(idx) diff --git a/mne/defaults.py b/mne/defaults.py index 27bfe96b26c..0340170f994 100644 --- a/mne/defaults.py +++ b/mne/defaults.py @@ -12,25 +12,27 @@ exci='k', ias='k', syst='k', seeg='saddlebrown', dbs='seagreen', dipole='k', gof='k', bio='k', ecog='k', hbo='#AA3377', hbr='b', fnirs_cw_amplitude='k', fnirs_fd_ac_amplitude='k', - fnirs_fd_phase='k', fnirs_od='k', csd='k', whitened='k'), + fnirs_fd_phase='k', fnirs_od='k', csd='k', whitened='k', + gsr='#666633', temperature='#663333'), si_units=dict(mag='T', grad='T/m', eeg='V', eog='V', ecg='V', emg='V', misc='AU', seeg='V', dbs='V', dipole='Am', gof='GOF', bio='V', ecog='V', hbo='M', hbr='M', ref_meg='T', fnirs_cw_amplitude='V', fnirs_fd_ac_amplitude='V', fnirs_fd_phase='rad', fnirs_od='V', csd='V/m²', - whitened='Z'), + whitened='Z', gsr='S', temperature='C'), units=dict(mag='fT', grad='fT/cm', eeg='µV', eog='µV', ecg='µV', emg='µV', misc='AU', seeg='mV', dbs='µV', dipole='nAm', gof='GOF', bio='µV', ecog='µV', hbo='µM', hbr='µM', ref_meg='fT', fnirs_cw_amplitude='V', fnirs_fd_ac_amplitude='V', fnirs_fd_phase='rad', fnirs_od='V', csd='mV/m²', - whitened='Z'), + whitened='Z', gsr='S', temperature='C'), # scalings for the units scalings=dict(mag=1e15, grad=1e13, eeg=1e6, eog=1e6, emg=1e6, ecg=1e6, misc=1.0, seeg=1e3, dbs=1e6, ecog=1e6, dipole=1e9, gof=1.0, bio=1e6, hbo=1e6, hbr=1e6, ref_meg=1e15, fnirs_cw_amplitude=1.0, fnirs_fd_ac_amplitude=1.0, - fnirs_fd_phase=1., fnirs_od=1.0, csd=1e3, whitened=1.), + fnirs_fd_phase=1., fnirs_od=1.0, csd=1e3, whitened=1., + gsr=1., temperature=1.), # rough guess for a good plot scalings_plot_raw=dict(mag=1e-12, grad=4e-11, eeg=20e-6, eog=150e-6, ecg=5e-4, emg=1e-3, ref_meg=1e-12, misc='auto', @@ -39,7 +41,8 @@ hbr=10e-6, whitened=10., fnirs_cw_amplitude=2e-2, fnirs_fd_ac_amplitude=2e-2, fnirs_fd_phase=2e-1, fnirs_od=2e-2, csd=200e-4, - dipole=1e-7, gof=1e2), + dipole=1e-7, gof=1e2, + gsr=1., temperature=1.), scalings_cov_rank=dict(mag=1e12, grad=1e11, eeg=1e5, # ~100x scalings seeg=1e1, dbs=1e4, ecog=1e4, hbo=1e4, hbr=1e4), ylim=dict(mag=(-600., 600.), grad=(-200., 200.), eeg=(-200., 200.), @@ -55,7 +58,8 @@ fnirs_fd_phase='fNIRS (FD phase)', fnirs_od='fNIRS (OD)', hbr='Deoxyhemoglobin', gof='Goodness of fit', csd='Current source density', - stim='Stimulus', + stim='Stimulus', gsr='Galvanic skin response', + temperature='Temperature', ), mask_params=dict(marker='o', markerfacecolor='w', diff --git a/mne/io/constants.py b/mne/io/constants.py index 9f3959004b0..e37204e36a0 100644 --- a/mne/io/constants.py +++ b/mne/io/constants.py @@ -202,6 +202,8 @@ FIFF.FIFFV_DIPOLE_WAVE = 1000 # Dipole time curve (xplotter/xfit) FIFF.FIFFV_GOODNESS_FIT = 1001 # Goodness of fit (xplotter/xfit) FIFF.FIFFV_FNIRS_CH = 1100 # Functional near-infrared spectroscopy +FIFF.FIFFV_TEMPERATURE_CH = 1200 # Functional near-infrared spectroscopy +FIFF.FIFFV_GALVANIC_CH = 1300 # Galvanic skin response _ch_kind_named = {key: key for key in ( FIFF.FIFFV_BIO_CH, FIFF.FIFFV_MEG_CH, @@ -223,6 +225,8 @@ FIFF.FIFFV_DIPOLE_WAVE, FIFF.FIFFV_GOODNESS_FIT, FIFF.FIFFV_FNIRS_CH, + FIFF.FIFFV_GALVANIC_CH, + FIFF.FIFFV_TEMPERATURE_CH, )} # @@ -839,7 +843,7 @@ FIFF.FIFF_UNIT_V = 107 # volt FIFF.FIFF_UNIT_F = 108 # farad FIFF.FIFF_UNIT_OHM = 109 # ohm -FIFF.FIFF_UNIT_MHO = 110 # one per ohm +FIFF.FIFF_UNIT_S = 110 # Siemens (same as Moh, what fiff-constants calls it) FIFF.FIFF_UNIT_WB = 111 # weber FIFF.FIFF_UNIT_T = 112 # tesla FIFF.FIFF_UNIT_H = 113 # Henry @@ -861,7 +865,7 @@ FIFF.FIFF_UNIT_CD, FIFF.FIFF_UNIT_MOL_M3, FIFF.FIFF_UNIT_HZ, FIFF.FIFF_UNIT_N, FIFF.FIFF_UNIT_PA, FIFF.FIFF_UNIT_J, FIFF.FIFF_UNIT_W, FIFF.FIFF_UNIT_C, FIFF.FIFF_UNIT_V, FIFF.FIFF_UNIT_F, FIFF.FIFF_UNIT_OHM, - FIFF.FIFF_UNIT_MHO, FIFF.FIFF_UNIT_WB, FIFF.FIFF_UNIT_T, FIFF.FIFF_UNIT_H, + FIFF.FIFF_UNIT_S, FIFF.FIFF_UNIT_WB, FIFF.FIFF_UNIT_T, FIFF.FIFF_UNIT_H, FIFF.FIFF_UNIT_CEL, FIFF.FIFF_UNIT_LM, FIFF.FIFF_UNIT_LX, FIFF.FIFF_UNIT_V_M2, FIFF.FIFF_UNIT_T_M, FIFF.FIFF_UNIT_AM, FIFF.FIFF_UNIT_AM_M2, FIFF.FIFF_UNIT_AM_M3, diff --git a/mne/io/pick.py b/mne/io/pick.py index 548e3a913dc..2d33cb6d7a6 100644 --- a/mne/io/pick.py +++ b/mne/io/pick.py @@ -94,7 +94,12 @@ def get_channel_type_constants(include_defaults=False): 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)) + coil_type=FIFF.FIFFV_COIL_EEG_CSD), + temperature=dict(kind=FIFF.FIFFV_TEMPERATURE_CH, + unit=FIFF.FIFF_UNIT_C), + gsr=dict(kind=FIFF.FIFFV_GALVANIC_CH, + unit=FIFF.FIFF_UNIT_S), + ) if include_defaults: coil_none = dict(coil_type=FIFF.FIFFV_COIL_NONE) unit_none = dict(unit=FIFF.FIFF_UNIT_NONE) @@ -146,6 +151,8 @@ def get_channel_type_constants(include_defaults=False): 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', } # How to reduce our categories in channel_type (originally) _second_rules = { @@ -186,7 +193,8 @@ def channel_type(info, idx): {'grad', 'mag', 'eeg', 'csd', 'stim', 'eog', 'emg', 'ecg', 'ref_meg', 'resp', 'exci', 'ias', 'syst', 'misc', 'seeg', 'dbs', - 'bio', 'chpi', 'dipole', 'gof', 'ecog', 'hbo', 'hbr'} + 'bio', 'chpi', 'dipole', 'gof', 'ecog', 'hbo', 'hbr', + 'temperature', 'gsr'} """ # 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 @@ -368,10 +376,11 @@ def _check_info_exclude(info, 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, include=(), exclude='bads', selection=None): + 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, + include=(), exclude='bads', selection=None): """Pick channels by type and names. Parameters @@ -399,7 +408,8 @@ def pick_types(info, meg=False, eeg=False, stim=False, eog=False, ecg=False, 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): + 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" and "fnirs") must be of type bool, not {}.') @@ -408,7 +418,8 @@ def pick_types(info, meg=False, eeg=False, stim=False, eog=False, ecg=False, 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) + gof=gof, bio=bio, ecog=ecog, csd=csd, + temperature=temperature, gsr=gsr) # avoid triage if possible if isinstance(meg, bool): for key in ('grad', 'mag'): @@ -911,7 +922,7 @@ def _check_excludes_includes(chs, info=None, allow_bads=False): 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) + dbs=True, temperature=False, gsr=False) _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', diff --git a/mne/io/tests/test_constants.py b/mne/io/tests/test_constants.py index b74c4ec3894..b334447993d 100644 --- a/mne/io/tests/test_constants.py +++ b/mne/io/tests/test_constants.py @@ -21,7 +21,7 @@ # https://github.com/mne-tools/fiff-constants/commits/master REPO = 'mne-tools' -COMMIT = 'aa49e20cff5791fbaf01d77ad4ec2e0ecb69840d' +COMMIT = '6d9ca9ce7fb44c63d429c2986a953500743dfb22' # These are oddities that we won't address: iod_dups = (355, 359) # these are in both MEGIN and MNE files diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 8f89057ad77..8181b856a44 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -2357,6 +2357,10 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): EEG-CSD channels. dbs : bool Deep brain stimulation channels. +temperature : bool + Temperature channels. +gsr : bool + Galvanic skin response channels. include : list of str List of additional channels to include. If empty do not include any. diff --git a/mne/viz/raw.py b/mne/viz/raw.py index a7ffa6e875c..8839654912f 100644 --- a/mne/viz/raw.py +++ b/mne/viz/raw.py @@ -565,7 +565,7 @@ def _setup_channel_selections(raw, kind, order): ecg=True, emg=True, ref_meg=False, misc=True, resp=True, chpi=True, exci=True, ias=True, syst=True, seeg=False, bio=True, ecog=False, fnirs=False, dbs=False, - exclude=()) + temperature=True, gsr=True, exclude=()) if len(misc) and np.in1d(misc, order).any(): selections_dict['Misc'] = misc return selections_dict