CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
nirs.py337 linesDownload Raw Back to nirs
1# Authors: The MNE-Python contributors.2# License: BSD-3-Clause3# Copyright the MNE-Python contributors.4 5import re6 7import numpy as np8 9from ..._fiff.pick import _picks_to_idx, pick_types10from ...utils import _check_option, _validate_type, fill_doc11 12# Standardized fNIRS channel name regexs13_S_D_F_RE = re.compile(r"S(\d+)_D(\d+) (\d+\.?\d*)")14_S_D_H_RE = re.compile(r"S(\d+)_D(\d+) (\w+)")15 16 17@fill_doc18def source_detector_distances(info, picks=None):19    r"""Determine the distance between NIRS source and detectors.20 21    Parameters22    ----------23    %(info_not_none)s24    %(picks_all_data)s25 26    Returns27    -------28    dists : array of float29        Array containing distances in meters.30        Of shape equal to number of channels, or shape of picks if supplied.31    """32    return np.array(33        [34            np.linalg.norm(35                np.diff(info["chs"][pick]["loc"][3:9].reshape(2, 3), axis=0)[0]36            )37            for pick in _picks_to_idx(info, picks, exclude=[])38        ],39        float,40    )41 42 43@fill_doc44def short_channels(info, threshold=0.01):45    r"""Determine which NIRS channels are short.46 47    Channels with a source to detector distance of less than48    ``threshold`` are reported as short. The default threshold is 0.01 m.49 50    Parameters51    ----------52    %(info_not_none)s53    threshold : float54        The threshold distance for what is considered short in meters.55 56    Returns57    -------58    short : array of bool59        Array indicating which channels are short.60        Of shape equal to number of channels.61    """62    return source_detector_distances(info) < threshold63 64 65def _channel_frequencies(info):66    """Return the light frequency for each channel."""67    # Only valid for fNIRS data before conversion to haemoglobin68    picks = _picks_to_idx(69        info, ["fnirs_cw_amplitude", "fnirs_od"], exclude=[], allow_empty=True70    )71    freqs = list()72    for pick in picks:73        freqs.append(round(float(_S_D_F_RE.match(info["ch_names"][pick]).groups()[2])))74    return np.array(freqs, int)75 76 77def _channel_chromophore(info):78    """Return the chromophore of each channel."""79    # Only valid for fNIRS data after conversion to haemoglobin80    picks = _picks_to_idx(info, ["hbo", "hbr"], exclude=[], allow_empty=True)81    chroma = []82    for ii in picks:83        chroma.append(info["ch_names"][ii].split(" ")[1])84    return chroma85 86 87def _check_channels_ordered(info, pair_vals, *, throw_errors=True, check_bads=True):88    """Check channels follow expected fNIRS format.89 90    If the channels are correctly ordered then an array of valid picks91    will be returned.92 93    If throw_errors is True then any errors in fNIRS formatting will be94    thrown to inform the user. If throw_errors is False then an empty array95    will be returned if the channels are not sufficiently formatted.96    """97    # Every second channel should be same SD pair98    # and have the specified light frequencies.99 100    # All wavelength based fNIRS data.101    picks_wave = _picks_to_idx(102        info, ["fnirs_cw_amplitude", "fnirs_od"], exclude=[], allow_empty=True103    )104    # All chromophore fNIRS data105    picks_chroma = _picks_to_idx(info, ["hbo", "hbr"], exclude=[], allow_empty=True)106 107    if (len(picks_wave) > 0) & (len(picks_chroma) > 0):108        picks = _throw_or_return_empty(109            "MNE does not support a combination of amplitude, optical "110            "density, and haemoglobin data in the same raw structure.",111            throw_errors,112        )113 114    # All continuous wave fNIRS data115    if len(picks_wave):116        error_word = "frequencies"117        use_RE = _S_D_F_RE118        picks = picks_wave119    else:120        error_word = "chromophore"121        use_RE = _S_D_H_RE122        picks = picks_chroma123 124    pair_vals = np.array(pair_vals)125    if pair_vals.shape != (2,):126        raise ValueError(127            f"Exactly two {error_word} must exist in info, got {list(pair_vals)}"128        )129    # In principle we do not need to require that these be sorted --130    # all we need to do is change our sorted() below to make use of a131    # pair_vals.index(...) in a sort key -- but in practice we always want132    # (hbo, hbr) or (lower_freq, upper_freq) pairings, both of which will133    # work with a naive string sort, so let's just enforce sorted-ness here134    is_str = pair_vals.dtype.kind == "U"135    pair_vals = list(pair_vals)136    if is_str:137        if pair_vals != ["hbo", "hbr"]:138            raise ValueError(139                f'The {error_word} in info must be ["hbo", "hbr"], but got '140                f"{pair_vals} instead"141            )142    elif not np.array_equal(np.unique(pair_vals), pair_vals):143        raise ValueError(144            f"The {error_word} in info must be unique and sorted, but got "145            f"got {pair_vals} instead"146        )147 148    if len(picks) % 2 != 0:149        picks = _throw_or_return_empty(150            "NIRS channels not ordered correctly. An even number of NIRS "151            f"channels is required. {len(info.ch_names)} channels were"152            f"provided",153            throw_errors,154        )155 156    # Ensure wavelength info exists for waveform data157    all_freqs = [info["chs"][ii]["loc"][9] for ii in picks_wave]158    if np.any(np.isnan(all_freqs)):159        picks = _throw_or_return_empty(160            f"NIRS channels is missing wavelength information in the "161            f'info["chs"] structure. The encoded wavelengths are {all_freqs}.',162            throw_errors,163        )164 165    # Validate the channel naming scheme166    for pick in picks:167        ch_name_info = use_RE.match(info["chs"][pick]["ch_name"])168        if not bool(ch_name_info):169            picks = _throw_or_return_empty(170                "NIRS channels have specified naming conventions. "171                "The provided channel name can not be parsed: "172                f"{repr(info.ch_names[pick])}",173                throw_errors,174            )175            break176        value = ch_name_info.groups()[2]177        if len(picks_wave):178            value = value179        else:  # picks_chroma180            if value not in ["hbo", "hbr"]:181                picks = _throw_or_return_empty(182                    "NIRS channels have specified naming conventions."183                    "Chromophore data must be labeled either hbo or hbr. "184                    f"The failing channel is {info['chs'][pick]['ch_name']}",185                    throw_errors,186                )187                break188 189    # Reorder to be paired (naive sort okay here given validation above)190    picks = picks[np.argsort([info["ch_names"][pick] for pick in picks])]191 192    # Validate our paired ordering193    for ii, jj in zip(picks[::2], picks[1::2]):194        ch1_name = info["chs"][ii]["ch_name"]195        ch2_name = info["chs"][jj]["ch_name"]196        ch1_re = use_RE.match(ch1_name)197        ch2_re = use_RE.match(ch2_name)198        ch1_S, ch1_D, ch1_value = ch1_re.groups()[:3]199        ch2_S, ch2_D, ch2_value = ch2_re.groups()[:3]200        if len(picks_wave):201            ch1_value, ch2_value = float(ch1_value), float(ch2_value)202        if (203            (ch1_S != ch2_S)204            or (ch1_D != ch2_D)205            or (ch1_value != pair_vals[0])206            or (ch2_value != pair_vals[1])207        ):208            picks = _throw_or_return_empty(209                "NIRS channels not ordered correctly. Channels must be "210                "ordered as source detector pairs with alternating"211                f" {error_word} {pair_vals[0]} & {pair_vals[1]}, but got "212                f"S{ch1_S}_D{ch1_D} pair "213                f"{repr(ch1_name)} and {repr(ch2_name)}",214                throw_errors,215            )216            break217 218    if check_bads:219        for ii, jj in zip(picks[::2], picks[1::2]):220            want = [info.ch_names[ii], info.ch_names[jj]]221            got = list(set(info["bads"]).intersection(want))222            if len(got) == 1:223                raise RuntimeError(224                    f"NIRS bad labelling is not consistent, found {got} but "225                    f"needed {want}"226                )227    return picks228 229 230def _throw_or_return_empty(msg, throw_errors):231    if throw_errors:232        raise ValueError(msg)233    else:234        return []235 236 237def _validate_nirs_info(238    info,239    *,240    throw_errors=True,241    fnirs=None,242    which=None,243    check_bads=True,244    allow_empty=True,245):246    """Apply all checks to fNIRS info. Works on all continuous wave types."""247    _validate_type(fnirs, (None, str), "fnirs")248    kinds = dict(249        od="optical density",250        cw_amplitude="continuous wave",251        hb="chromophore",252    )253    _check_option("fnirs", fnirs, (None,) + tuple(kinds))254    if fnirs is not None:255        kind = kinds[fnirs]256        fnirs = ["hbo", "hbr"] if fnirs == "hb" else f"fnirs_{fnirs}"257        if not len(pick_types(info, fnirs=fnirs)):258            raise RuntimeError(259                f"{which} must operate on {kind} data, but none was found."260            )261    freqs = np.unique(_channel_frequencies(info))262    if freqs.size > 0:263        pair_vals = freqs264    else:265        pair_vals = np.unique(_channel_chromophore(info))266    out = _check_channels_ordered(267        info, pair_vals, throw_errors=throw_errors, check_bads=check_bads268    )269    return out270 271 272def _fnirs_spread_bads(info):273    """Spread bad labeling across fnirs channels."""274    # For an optode pair if any component (light frequency or chroma) is marked275    # as bad, then they all should be. This function will find any pairs marked276    # as bad and spread the bad marking to all components of the optode pair.277    picks = _validate_nirs_info(info, check_bads=False)278    new_bads = set(info["bads"])279    for ii, jj in zip(picks[::2], picks[1::2]):280        ch1_name, ch2_name = info.ch_names[ii], info.ch_names[jj]281        if ch1_name in new_bads:282            new_bads.add(ch2_name)283        elif ch2_name in new_bads:284            new_bads.add(ch1_name)285    info["bads"] = sorted(new_bads)286 287    return info288 289 290def _fnirs_optode_names(info):291    """Return list of unique optode names."""292    picks_wave = _picks_to_idx(293        info, ["fnirs_cw_amplitude", "fnirs_od"], exclude=[], allow_empty=True294    )295    picks_chroma = _picks_to_idx(info, ["hbo", "hbr"], exclude=[], allow_empty=True)296 297    if len(picks_wave) > 0:298        regex = _S_D_F_RE299    elif len(picks_chroma) > 0:300        regex = _S_D_H_RE301    else:302        return [], []303 304    sources = np.unique([int(regex.match(ch).groups()[0]) for ch in info.ch_names])305    detectors = np.unique([int(regex.match(ch).groups()[1]) for ch in info.ch_names])306 307    src_names = [f"S{s}" for s in sources]308    det_names = [f"D{d}" for d in detectors]309 310    return src_names, det_names311 312 313def _optode_position(info, optode):314    """Find the position of an optode."""315    idx = [optode in a for a in info.ch_names].index(True)316 317    if "S" in optode:318        loc_idx = range(3, 6)319    elif "D" in optode:320        loc_idx = range(6, 9)321 322    return info["chs"][idx]["loc"][loc_idx]323 324 325def _reorder_nirx(raw):326    # Maybe someday we should make this public like327    # mne.preprocessing.nirs.reorder_standard(raw, order='nirx')328    info = raw.info329    picks = pick_types(info, fnirs=True, exclude=[])330    prefixes = [info["ch_names"][pick].split()[0] for pick in picks]331    nirs_names = [info["ch_names"][pick] for pick in picks]332    nirs_sorted = sorted(333        nirs_names,334        key=lambda name: (prefixes.index(name.split()[0]), name.split(maxsplit=1)[1]),335    )336    raw.reorder_channels(nirs_sorted)337 
Aluode/PerceptionLabPortable · CoolFace