针对pulse-transit的工具
This commit is contained in:
		
							
								
								
									
										336
									
								
								dist/client/mne/preprocessing/nirs/nirs.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										336
									
								
								dist/client/mne/preprocessing/nirs/nirs.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,336 @@
 | 
			
		||||
# Authors: The MNE-Python contributors.
 | 
			
		||||
# License: BSD-3-Clause
 | 
			
		||||
# Copyright the MNE-Python contributors.
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
import numpy as np
 | 
			
		||||
 | 
			
		||||
from ..._fiff.pick import _picks_to_idx, pick_types
 | 
			
		||||
from ...utils import _check_option, _validate_type, fill_doc
 | 
			
		||||
 | 
			
		||||
# Standardized fNIRS channel name regexs
 | 
			
		||||
_S_D_F_RE = re.compile(r"S(\d+)_D(\d+) (\d+\.?\d*)")
 | 
			
		||||
_S_D_H_RE = re.compile(r"S(\d+)_D(\d+) (\w+)")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@fill_doc
 | 
			
		||||
def source_detector_distances(info, picks=None):
 | 
			
		||||
    r"""Determine the distance between NIRS source and detectors.
 | 
			
		||||
 | 
			
		||||
    Parameters
 | 
			
		||||
    ----------
 | 
			
		||||
    %(info_not_none)s
 | 
			
		||||
    %(picks_all_data)s
 | 
			
		||||
 | 
			
		||||
    Returns
 | 
			
		||||
    -------
 | 
			
		||||
    dists : array of float
 | 
			
		||||
        Array containing distances in meters.
 | 
			
		||||
        Of shape equal to number of channels, or shape of picks if supplied.
 | 
			
		||||
    """
 | 
			
		||||
    return np.array(
 | 
			
		||||
        [
 | 
			
		||||
            np.linalg.norm(
 | 
			
		||||
                np.diff(info["chs"][pick]["loc"][3:9].reshape(2, 3), axis=0)[0]
 | 
			
		||||
            )
 | 
			
		||||
            for pick in _picks_to_idx(info, picks, exclude=[])
 | 
			
		||||
        ],
 | 
			
		||||
        float,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@fill_doc
 | 
			
		||||
def short_channels(info, threshold=0.01):
 | 
			
		||||
    r"""Determine which NIRS channels are short.
 | 
			
		||||
 | 
			
		||||
    Channels with a source to detector distance of less than
 | 
			
		||||
    ``threshold`` are reported as short. The default threshold is 0.01 m.
 | 
			
		||||
 | 
			
		||||
    Parameters
 | 
			
		||||
    ----------
 | 
			
		||||
    %(info_not_none)s
 | 
			
		||||
    threshold : float
 | 
			
		||||
        The threshold distance for what is considered short in meters.
 | 
			
		||||
 | 
			
		||||
    Returns
 | 
			
		||||
    -------
 | 
			
		||||
    short : array of bool
 | 
			
		||||
        Array indicating which channels are short.
 | 
			
		||||
        Of shape equal to number of channels.
 | 
			
		||||
    """
 | 
			
		||||
    return source_detector_distances(info) < threshold
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _channel_frequencies(info):
 | 
			
		||||
    """Return the light frequency for each channel."""
 | 
			
		||||
    # Only valid for fNIRS data before conversion to haemoglobin
 | 
			
		||||
    picks = _picks_to_idx(
 | 
			
		||||
        info, ["fnirs_cw_amplitude", "fnirs_od"], exclude=[], allow_empty=True
 | 
			
		||||
    )
 | 
			
		||||
    freqs = list()
 | 
			
		||||
    for pick in picks:
 | 
			
		||||
        freqs.append(round(float(_S_D_F_RE.match(info["ch_names"][pick]).groups()[2])))
 | 
			
		||||
    return np.array(freqs, int)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _channel_chromophore(info):
 | 
			
		||||
    """Return the chromophore of each channel."""
 | 
			
		||||
    # Only valid for fNIRS data after conversion to haemoglobin
 | 
			
		||||
    picks = _picks_to_idx(info, ["hbo", "hbr"], exclude=[], allow_empty=True)
 | 
			
		||||
    chroma = []
 | 
			
		||||
    for ii in picks:
 | 
			
		||||
        chroma.append(info["ch_names"][ii].split(" ")[1])
 | 
			
		||||
    return chroma
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _check_channels_ordered(info, pair_vals, *, throw_errors=True, check_bads=True):
 | 
			
		||||
    """Check channels follow expected fNIRS format.
 | 
			
		||||
 | 
			
		||||
    If the channels are correctly ordered then an array of valid picks
 | 
			
		||||
    will be returned.
 | 
			
		||||
 | 
			
		||||
    If throw_errors is True then any errors in fNIRS formatting will be
 | 
			
		||||
    thrown to inform the user. If throw_errors is False then an empty array
 | 
			
		||||
    will be returned if the channels are not sufficiently formatted.
 | 
			
		||||
    """
 | 
			
		||||
    # Every second channel should be same SD pair
 | 
			
		||||
    # and have the specified light frequencies.
 | 
			
		||||
 | 
			
		||||
    # All wavelength based fNIRS data.
 | 
			
		||||
    picks_wave = _picks_to_idx(
 | 
			
		||||
        info, ["fnirs_cw_amplitude", "fnirs_od"], exclude=[], allow_empty=True
 | 
			
		||||
    )
 | 
			
		||||
    # All chromophore fNIRS data
 | 
			
		||||
    picks_chroma = _picks_to_idx(info, ["hbo", "hbr"], exclude=[], allow_empty=True)
 | 
			
		||||
 | 
			
		||||
    if (len(picks_wave) > 0) & (len(picks_chroma) > 0):
 | 
			
		||||
        picks = _throw_or_return_empty(
 | 
			
		||||
            "MNE does not support a combination of amplitude, optical "
 | 
			
		||||
            "density, and haemoglobin data in the same raw structure.",
 | 
			
		||||
            throw_errors,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # All continuous wave fNIRS data
 | 
			
		||||
    if len(picks_wave):
 | 
			
		||||
        error_word = "frequencies"
 | 
			
		||||
        use_RE = _S_D_F_RE
 | 
			
		||||
        picks = picks_wave
 | 
			
		||||
    else:
 | 
			
		||||
        error_word = "chromophore"
 | 
			
		||||
        use_RE = _S_D_H_RE
 | 
			
		||||
        picks = picks_chroma
 | 
			
		||||
 | 
			
		||||
    pair_vals = np.array(pair_vals)
 | 
			
		||||
    if pair_vals.shape != (2,):
 | 
			
		||||
        raise ValueError(
 | 
			
		||||
            f"Exactly two {error_word} must exist in info, got {list(pair_vals)}"
 | 
			
		||||
        )
 | 
			
		||||
    # In principle we do not need to require that these be sorted --
 | 
			
		||||
    # all we need to do is change our sorted() below to make use of a
 | 
			
		||||
    # pair_vals.index(...) in a sort key -- but in practice we always want
 | 
			
		||||
    # (hbo, hbr) or (lower_freq, upper_freq) pairings, both of which will
 | 
			
		||||
    # work with a naive string sort, so let's just enforce sorted-ness here
 | 
			
		||||
    is_str = pair_vals.dtype.kind == "U"
 | 
			
		||||
    pair_vals = list(pair_vals)
 | 
			
		||||
    if is_str:
 | 
			
		||||
        if pair_vals != ["hbo", "hbr"]:
 | 
			
		||||
            raise ValueError(
 | 
			
		||||
                f'The {error_word} in info must be ["hbo", "hbr"], but got '
 | 
			
		||||
                f"{pair_vals} instead"
 | 
			
		||||
            )
 | 
			
		||||
    elif not np.array_equal(np.unique(pair_vals), pair_vals):
 | 
			
		||||
        raise ValueError(
 | 
			
		||||
            f"The {error_word} in info must be unique and sorted, but got "
 | 
			
		||||
            f"got {pair_vals} instead"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if len(picks) % 2 != 0:
 | 
			
		||||
        picks = _throw_or_return_empty(
 | 
			
		||||
            "NIRS channels not ordered correctly. An even number of NIRS "
 | 
			
		||||
            f"channels is required. {len(info.ch_names)} channels were"
 | 
			
		||||
            f"provided",
 | 
			
		||||
            throw_errors,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Ensure wavelength info exists for waveform data
 | 
			
		||||
    all_freqs = [info["chs"][ii]["loc"][9] for ii in picks_wave]
 | 
			
		||||
    if np.any(np.isnan(all_freqs)):
 | 
			
		||||
        picks = _throw_or_return_empty(
 | 
			
		||||
            f"NIRS channels is missing wavelength information in the "
 | 
			
		||||
            f'info["chs"] structure. The encoded wavelengths are {all_freqs}.',
 | 
			
		||||
            throw_errors,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Validate the channel naming scheme
 | 
			
		||||
    for pick in picks:
 | 
			
		||||
        ch_name_info = use_RE.match(info["chs"][pick]["ch_name"])
 | 
			
		||||
        if not bool(ch_name_info):
 | 
			
		||||
            picks = _throw_or_return_empty(
 | 
			
		||||
                "NIRS channels have specified naming conventions. "
 | 
			
		||||
                "The provided channel name can not be parsed: "
 | 
			
		||||
                f"{repr(info.ch_names[pick])}",
 | 
			
		||||
                throw_errors,
 | 
			
		||||
            )
 | 
			
		||||
            break
 | 
			
		||||
        value = ch_name_info.groups()[2]
 | 
			
		||||
        if len(picks_wave):
 | 
			
		||||
            value = value
 | 
			
		||||
        else:  # picks_chroma
 | 
			
		||||
            if value not in ["hbo", "hbr"]:
 | 
			
		||||
                picks = _throw_or_return_empty(
 | 
			
		||||
                    "NIRS channels have specified naming conventions."
 | 
			
		||||
                    "Chromophore data must be labeled either hbo or hbr. "
 | 
			
		||||
                    f"The failing channel is {info['chs'][pick]['ch_name']}",
 | 
			
		||||
                    throw_errors,
 | 
			
		||||
                )
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
    # Reorder to be paired (naive sort okay here given validation above)
 | 
			
		||||
    picks = picks[np.argsort([info["ch_names"][pick] for pick in picks])]
 | 
			
		||||
 | 
			
		||||
    # Validate our paired ordering
 | 
			
		||||
    for ii, jj in zip(picks[::2], picks[1::2]):
 | 
			
		||||
        ch1_name = info["chs"][ii]["ch_name"]
 | 
			
		||||
        ch2_name = info["chs"][jj]["ch_name"]
 | 
			
		||||
        ch1_re = use_RE.match(ch1_name)
 | 
			
		||||
        ch2_re = use_RE.match(ch2_name)
 | 
			
		||||
        ch1_S, ch1_D, ch1_value = ch1_re.groups()[:3]
 | 
			
		||||
        ch2_S, ch2_D, ch2_value = ch2_re.groups()[:3]
 | 
			
		||||
        if len(picks_wave):
 | 
			
		||||
            ch1_value, ch2_value = float(ch1_value), float(ch2_value)
 | 
			
		||||
        if (
 | 
			
		||||
            (ch1_S != ch2_S)
 | 
			
		||||
            or (ch1_D != ch2_D)
 | 
			
		||||
            or (ch1_value != pair_vals[0])
 | 
			
		||||
            or (ch2_value != pair_vals[1])
 | 
			
		||||
        ):
 | 
			
		||||
            picks = _throw_or_return_empty(
 | 
			
		||||
                "NIRS channels not ordered correctly. Channels must be "
 | 
			
		||||
                "ordered as source detector pairs with alternating"
 | 
			
		||||
                f" {error_word} {pair_vals[0]} & {pair_vals[1]}, but got "
 | 
			
		||||
                f"S{ch1_S}_D{ch1_D} pair "
 | 
			
		||||
                f"{repr(ch1_name)} and {repr(ch2_name)}",
 | 
			
		||||
                throw_errors,
 | 
			
		||||
            )
 | 
			
		||||
            break
 | 
			
		||||
 | 
			
		||||
    if check_bads:
 | 
			
		||||
        for ii, jj in zip(picks[::2], picks[1::2]):
 | 
			
		||||
            want = [info.ch_names[ii], info.ch_names[jj]]
 | 
			
		||||
            got = list(set(info["bads"]).intersection(want))
 | 
			
		||||
            if len(got) == 1:
 | 
			
		||||
                raise RuntimeError(
 | 
			
		||||
                    f"NIRS bad labelling is not consistent, found {got} but "
 | 
			
		||||
                    f"needed {want}"
 | 
			
		||||
                )
 | 
			
		||||
    return picks
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _throw_or_return_empty(msg, throw_errors):
 | 
			
		||||
    if throw_errors:
 | 
			
		||||
        raise ValueError(msg)
 | 
			
		||||
    else:
 | 
			
		||||
        return []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _validate_nirs_info(
 | 
			
		||||
    info,
 | 
			
		||||
    *,
 | 
			
		||||
    throw_errors=True,
 | 
			
		||||
    fnirs=None,
 | 
			
		||||
    which=None,
 | 
			
		||||
    check_bads=True,
 | 
			
		||||
    allow_empty=True,
 | 
			
		||||
):
 | 
			
		||||
    """Apply all checks to fNIRS info. Works on all continuous wave types."""
 | 
			
		||||
    _validate_type(fnirs, (None, str), "fnirs")
 | 
			
		||||
    kinds = dict(
 | 
			
		||||
        od="optical density",
 | 
			
		||||
        cw_amplitude="continuous wave",
 | 
			
		||||
        hb="chromophore",
 | 
			
		||||
    )
 | 
			
		||||
    _check_option("fnirs", fnirs, (None,) + tuple(kinds))
 | 
			
		||||
    if fnirs is not None:
 | 
			
		||||
        kind = kinds[fnirs]
 | 
			
		||||
        fnirs = ["hbo", "hbr"] if fnirs == "hb" else f"fnirs_{fnirs}"
 | 
			
		||||
        if not len(pick_types(info, fnirs=fnirs)):
 | 
			
		||||
            raise RuntimeError(
 | 
			
		||||
                f"{which} must operate on {kind} data, but none was found."
 | 
			
		||||
            )
 | 
			
		||||
    freqs = np.unique(_channel_frequencies(info))
 | 
			
		||||
    if freqs.size > 0:
 | 
			
		||||
        pair_vals = freqs
 | 
			
		||||
    else:
 | 
			
		||||
        pair_vals = np.unique(_channel_chromophore(info))
 | 
			
		||||
    out = _check_channels_ordered(
 | 
			
		||||
        info, pair_vals, throw_errors=throw_errors, check_bads=check_bads
 | 
			
		||||
    )
 | 
			
		||||
    return out
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _fnirs_spread_bads(info):
 | 
			
		||||
    """Spread bad labeling across fnirs channels."""
 | 
			
		||||
    # For an optode pair if any component (light frequency or chroma) is marked
 | 
			
		||||
    # as bad, then they all should be. This function will find any pairs marked
 | 
			
		||||
    # as bad and spread the bad marking to all components of the optode pair.
 | 
			
		||||
    picks = _validate_nirs_info(info, check_bads=False)
 | 
			
		||||
    new_bads = set(info["bads"])
 | 
			
		||||
    for ii, jj in zip(picks[::2], picks[1::2]):
 | 
			
		||||
        ch1_name, ch2_name = info.ch_names[ii], info.ch_names[jj]
 | 
			
		||||
        if ch1_name in new_bads:
 | 
			
		||||
            new_bads.add(ch2_name)
 | 
			
		||||
        elif ch2_name in new_bads:
 | 
			
		||||
            new_bads.add(ch1_name)
 | 
			
		||||
    info["bads"] = sorted(new_bads)
 | 
			
		||||
 | 
			
		||||
    return info
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _fnirs_optode_names(info):
 | 
			
		||||
    """Return list of unique optode names."""
 | 
			
		||||
    picks_wave = _picks_to_idx(
 | 
			
		||||
        info, ["fnirs_cw_amplitude", "fnirs_od"], exclude=[], allow_empty=True
 | 
			
		||||
    )
 | 
			
		||||
    picks_chroma = _picks_to_idx(info, ["hbo", "hbr"], exclude=[], allow_empty=True)
 | 
			
		||||
 | 
			
		||||
    if len(picks_wave) > 0:
 | 
			
		||||
        regex = _S_D_F_RE
 | 
			
		||||
    elif len(picks_chroma) > 0:
 | 
			
		||||
        regex = _S_D_H_RE
 | 
			
		||||
    else:
 | 
			
		||||
        return [], []
 | 
			
		||||
 | 
			
		||||
    sources = np.unique([int(regex.match(ch).groups()[0]) for ch in info.ch_names])
 | 
			
		||||
    detectors = np.unique([int(regex.match(ch).groups()[1]) for ch in info.ch_names])
 | 
			
		||||
 | 
			
		||||
    src_names = [f"S{s}" for s in sources]
 | 
			
		||||
    det_names = [f"D{d}" for d in detectors]
 | 
			
		||||
 | 
			
		||||
    return src_names, det_names
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _optode_position(info, optode):
 | 
			
		||||
    """Find the position of an optode."""
 | 
			
		||||
    idx = [optode in a for a in info.ch_names].index(True)
 | 
			
		||||
 | 
			
		||||
    if "S" in optode:
 | 
			
		||||
        loc_idx = range(3, 6)
 | 
			
		||||
    elif "D" in optode:
 | 
			
		||||
        loc_idx = range(6, 9)
 | 
			
		||||
 | 
			
		||||
    return info["chs"][idx]["loc"][loc_idx]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _reorder_nirx(raw):
 | 
			
		||||
    # Maybe someday we should make this public like
 | 
			
		||||
    # mne.preprocessing.nirs.reorder_standard(raw, order='nirx')
 | 
			
		||||
    info = raw.info
 | 
			
		||||
    picks = pick_types(info, fnirs=True, exclude=[])
 | 
			
		||||
    prefixes = [info["ch_names"][pick].split()[0] for pick in picks]
 | 
			
		||||
    nirs_names = [info["ch_names"][pick] for pick in picks]
 | 
			
		||||
    nirs_sorted = sorted(
 | 
			
		||||
        nirs_names,
 | 
			
		||||
        key=lambda name: (prefixes.index(name.split()[0]), name.split(maxsplit=1)[1]),
 | 
			
		||||
    )
 | 
			
		||||
    raw.reorder_channels(nirs_sorted)
 | 
			
		||||
		Reference in New Issue
	
	Block a user