def test_fnirs_channel_naming_and_order_custom_optical_density(): """Ensure fNIRS channel checking on manually created data.""" data = np.random.normal(size=(6, 10)) # Start with a correctly named raw intensity dataset # These are the steps required to build an fNIRS Raw object from scratch ch_names = ['S1_D1 760', 'S1_D1 850', 'S2_D1 760', 'S2_D1 850', 'S3_D1 760', 'S3_D1 850'] ch_types = np.repeat("fnirs_od", 6) info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=1.0) raw = RawArray(data, info, verbose=True) freqs = np.tile([760, 850], 3) for idx, f in enumerate(freqs): raw.info["chs"][idx]["loc"][9] = f freqs = np.unique(_channel_frequencies(raw)) picks = _check_channels_ordered(raw, freqs) assert len(picks) == len(raw.ch_names) assert len(picks) == 6 # Check block naming for optical density ch_names = ['S1_D1 760', 'S2_D1 760', 'S3_D1 760', 'S1_D1 850', 'S2_D1 850', 'S3_D1 850'] ch_types = np.repeat("fnirs_od", 6) info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=1.0) raw = RawArray(data, info, verbose=True) freqs = np.repeat([760, 850], 3) for idx, f in enumerate(freqs): raw.info["chs"][idx]["loc"][9] = f with pytest.raises(ValueError, match='channels not ordered correctly'): _check_channels_ordered(raw, [760, 850]) # and this is how you would fix the ordering, then it should pass raw.pick(picks=[0, 3, 1, 4, 2, 5]) _check_channels_ordered(raw, [760, 850])
def test_fnirs_channel_naming_and_order_readers(fname): """Ensure fNIRS channel checking on standard readers.""" # fNIRS data requires specific channel naming and ordering. # All standard readers should pass tests raw = read_raw_nirx(fname) freqs = np.unique(_channel_frequencies(raw.info)) assert_array_equal(freqs, [760, 850]) chroma = np.unique(_channel_chromophore(raw.info)) assert len(chroma) == 0 picks = _check_channels_ordered(raw.info, freqs) assert len(picks) == len(raw.ch_names) # as all fNIRS only data # Check that dropped channels are detected # For each source detector pair there must be two channels, # removing one should throw an error. raw_dropped = raw.copy().drop_channels(raw.ch_names[4]) with pytest.raises(ValueError, match='not ordered correctly'): _check_channels_ordered(raw_dropped.info, freqs) # The ordering must be increasing for the pairs, if provided raw_names_reversed = raw.copy().ch_names raw_names_reversed.reverse() raw_reversed = raw.copy().pick_channels(raw_names_reversed, ordered=True) with pytest.raises(ValueError, match='The frequencies.*sorted.*'): _check_channels_ordered(raw_reversed.info, [850, 760]) # So if we flip the second argument it should pass again picks = _check_channels_ordered(raw_reversed.info, freqs) got_first = set(raw_reversed.ch_names[pick].split()[1] for pick in picks[::2]) assert got_first == {'760'} got_second = set(raw_reversed.ch_names[pick].split()[1] for pick in picks[1::2]) assert got_second == {'850'} # Check on OD data raw = optical_density(raw) freqs = np.unique(_channel_frequencies(raw.info)) assert_array_equal(freqs, [760, 850]) chroma = np.unique(_channel_chromophore(raw.info)) assert len(chroma) == 0 picks = _check_channels_ordered(raw.info, freqs) assert len(picks) == len(raw.ch_names) # as all fNIRS only data # Check on haemoglobin data raw = beer_lambert_law(raw) freqs = np.unique(_channel_frequencies(raw.info)) assert len(freqs) == 0 assert len(_channel_chromophore(raw.info)) == len(raw.ch_names) chroma = np.unique(_channel_chromophore(raw.info)) assert_array_equal(chroma, ["hbo", "hbr"]) picks = _check_channels_ordered(raw.info, chroma) assert len(picks) == len(raw.ch_names) with pytest.raises(ValueError, match='chromophore in info'): _check_channels_ordered(raw.info, ["hbr", "hbo"])
def _interpolate_bads_nirs(inst, method='nearest', verbose=None): """Interpolate bad nirs channels. Simply replaces by closest non bad. Parameters ---------- inst : mne.io.Raw, mne.Epochs or mne.Evoked The data to interpolate. Must be preloaded. method : str Only the method 'nearest' is currently available. This method replaces each bad channel with the nearest non bad channel. %(verbose)s """ from scipy.spatial.distance import pdist, squareform from mne.preprocessing.nirs import _channel_frequencies,\ _check_channels_ordered # Returns pick of all nirs and ensures channels are correctly ordered freqs = np.unique(_channel_frequencies(inst)) picks_nirs = _check_channels_ordered(inst, freqs) if len(picks_nirs) == 0: return nirs_ch_names = [inst.info['ch_names'][p] for p in picks_nirs] bads_nirs = [ch for ch in inst.info['bads'] if ch in nirs_ch_names] if len(bads_nirs) == 0: return picks_bad = pick_channels(inst.info['ch_names'], bads_nirs, exclude=[]) bads_mask = [p in picks_bad for p in picks_nirs] chs = [inst.info['chs'][i] for i in picks_nirs] locs3d = np.array([ch['loc'][:3] for ch in chs]) _check_option('fnirs_method', method, ['nearest']) if method == 'nearest': dist = pdist(locs3d) dist = squareform(dist) for bad in picks_bad: dists_to_bad = dist[bad] # Ignore distances to self dists_to_bad[dists_to_bad == 0] = np.inf # Ignore distances to other bad channels dists_to_bad[bads_mask] = np.inf # Find closest remaining channels for same frequency closest_idx = np.argmin(dists_to_bad) + (bad % 2) inst._data[bad] = inst._data[closest_idx] inst.info['bads'] = [] return inst
def test_fnirs_channel_naming_and_order_custom_optical_density(): """Ensure fNIRS channel checking on manually created data.""" data = np.random.normal(size=(6, 10)) # Start with a correctly named raw intensity dataset # These are the steps required to build an fNIRS Raw object from scratch ch_names = [ 'S1_D1 760', 'S1_D1 850', 'S2_D1 760', 'S2_D1 850', 'S3_D1 760', 'S3_D1 850' ] ch_types = np.repeat("fnirs_od", 6) info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=1.0) raw = RawArray(data, info, verbose=True) freqs = np.tile([760, 850], 3) for idx, f in enumerate(freqs): raw.info["chs"][idx]["loc"][9] = f freqs = np.unique(_channel_frequencies(raw.info)) picks = _check_channels_ordered(raw.info, freqs) assert len(picks) == len(raw.ch_names) assert len(picks) == 6 # Check block naming for optical density ch_names = [ 'S1_D1 760', 'S2_D1 760', 'S3_D1 760', 'S1_D1 850', 'S2_D1 850', 'S3_D1 850' ] ch_types = np.repeat("fnirs_od", 6) info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=1.0) raw = RawArray(data, info, verbose=True) freqs = np.repeat([760, 850], 3) for idx, f in enumerate(freqs): raw.info["chs"][idx]["loc"][9] = f # no problems here _check_channels_ordered(raw.info, [760, 850]) # or with this (nirx) reordering raw.pick(picks=[0, 3, 1, 4, 2, 5]) _check_channels_ordered(raw.info, [760, 850]) # Check that if you mix types you get an error ch_names = [ 'S1_D1 hbo', 'S1_D1 hbr', 'S2_D1 hbo', 'S2_D1 hbr', 'S3_D1 hbo', 'S3_D1 hbr' ] ch_types = np.tile(["hbo", "hbr"], 3) info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=1.0) raw2 = RawArray(data, info, verbose=True) raw.add_channels([raw2]) with pytest.raises(ValueError, match='does not support a combination'): _check_channels_ordered(raw.info, [760, 850])
def _interpolate_bads_nirs(inst, method='nearest', exclude=(), verbose=None): from scipy.spatial.distance import pdist, squareform from mne.preprocessing.nirs import _channel_frequencies,\ _check_channels_ordered # Returns pick of all nirs and ensures channels are correctly ordered freqs = np.unique(_channel_frequencies(inst.info)) picks_nirs = _check_channels_ordered(inst.info, freqs) if len(picks_nirs) == 0: return nirs_ch_names = [inst.info['ch_names'][p] for p in picks_nirs] nirs_ch_names = [ch for ch in nirs_ch_names if ch not in exclude] bads_nirs = [ch for ch in inst.info['bads'] if ch in nirs_ch_names] if len(bads_nirs) == 0: return picks_bad = pick_channels(inst.info['ch_names'], bads_nirs, exclude=[]) bads_mask = [p in picks_bad for p in picks_nirs] chs = [inst.info['chs'][i] for i in picks_nirs] locs3d = np.array([ch['loc'][:3] for ch in chs]) _check_option('fnirs_method', method, ['nearest']) if method == 'nearest': dist = pdist(locs3d) dist = squareform(dist) for bad in picks_bad: dists_to_bad = dist[bad] # Ignore distances to self dists_to_bad[dists_to_bad == 0] = np.inf # Ignore distances to other bad channels dists_to_bad[bads_mask] = np.inf # Find closest remaining channels for same frequency closest_idx = np.argmin(dists_to_bad) + (bad % 2) inst._data[bad] = inst._data[closest_idx] inst.info['bads'] = [ch for ch in inst.info['bads'] if ch in exclude] return inst
def test_fnirs_channel_naming_and_order_custom_chroma(): """Ensure fNIRS channel checking on manually created data.""" data = np.random.RandomState(0).randn(6, 10) # Start with a correctly named raw intensity dataset # These are the steps required to build an fNIRS Raw object from scratch ch_names = [ 'S1_D1 hbo', 'S1_D1 hbr', 'S2_D1 hbo', 'S2_D1 hbr', 'S3_D1 hbo', 'S3_D1 hbr' ] ch_types = np.tile(["hbo", "hbr"], 3) info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=1.0) raw = RawArray(data, info, verbose=True) chroma = np.unique(_channel_chromophore(raw.info)) picks = _check_channels_ordered(raw.info, chroma) assert len(picks) == len(raw.ch_names) assert len(picks) == 6 # Test block creation fails ch_names = [ 'S1_D1 hbo', 'S2_D1 hbo', 'S3_D1 hbo', 'S1_D1 hbr', 'S2_D1 hbr', 'S3_D1 hbr' ] ch_types = np.repeat(["hbo", "hbr"], 3) info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=1.0) raw = RawArray(data, info, verbose=True) with pytest.raises(ValueError, match='not ordered .* chromophore'): _check_channels_ordered(raw.info, ["hbo", "hbr"]) # Reordering should fix raw.pick(picks=[0, 3, 1, 4, 2, 5]) _check_channels_ordered(raw.info, ["hbo", "hbr"]) # Wrong names should fail with pytest.raises(ValueError, match='not ordered .* chromophore'): _check_channels_ordered(raw.info, ["hbb", "hbr"]) # Test weird naming ch_names = [ 'S1_D1 hbb', 'S1_D1 hbr', 'S2_D1 hbb', 'S2_D1 hbr', 'S3_D1 hbb', 'S3_D1 hbr' ] ch_types = np.tile(["hbo", "hbr"], 3) info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=1.0) raw = RawArray(data, info, verbose=True) with pytest.raises(ValueError, match='naming conventions'): _check_channels_ordered(raw.info, ["hbb", "hbr"]) # Check more weird naming ch_names = [ 'S1_DX hbo', 'S1_DX hbr', 'S2_D1 hbo', 'S2_D1 hbr', 'S3_D1 hbo', 'S3_D1 hbr' ] ch_types = np.tile(["hbo", "hbr"], 3) info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=1.0) raw = RawArray(data, info, verbose=True) with pytest.raises(ValueError, match='can not be parsed'): _check_channels_ordered(raw.info, ["hbo", "hbr"])
def test_fnirs_channel_naming_and_order_custom_raw(): """Ensure fNIRS channel checking on manually created data.""" data = np.random.normal(size=(6, 10)) # Start with a correctly named raw intensity dataset # These are the steps required to build an fNIRS Raw object from scratch ch_names = [ 'S1_D1 760', 'S1_D1 850', 'S2_D1 760', 'S2_D1 850', 'S3_D1 760', 'S3_D1 850' ] ch_types = np.repeat("fnirs_cw_amplitude", 6) info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=1.0) raw = RawArray(data, info, verbose=True) freqs = np.tile([760, 850], 3) for idx, f in enumerate(freqs): raw.info["chs"][idx]["loc"][9] = f freqs = np.unique(_channel_frequencies(raw.info)) picks = _check_channels_ordered(raw.info, freqs) assert len(picks) == len(raw.ch_names) assert len(picks) == 6 # Different systems use different frequencies, so ensure that works ch_names = [ 'S1_D1 920', 'S1_D1 850', 'S2_D1 920', 'S2_D1 850', 'S3_D1 920', 'S3_D1 850' ] ch_types = np.repeat("fnirs_cw_amplitude", 6) info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=1.0) raw = RawArray(data, info, verbose=True) freqs = np.tile([920, 850], 3) for idx, f in enumerate(freqs): raw.info["chs"][idx]["loc"][9] = f picks = _check_channels_ordered(raw.info, [920, 850]) assert len(picks) == len(raw.ch_names) assert len(picks) == 6 # Catch expected errors # The frequencies named in the channel names must match the info loc field ch_names = [ 'S1_D1 760', 'S1_D1 850', 'S2_D1 760', 'S2_D1 850', 'S3_D1 760', 'S3_D1 850' ] ch_types = np.repeat("fnirs_cw_amplitude", 6) info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=1.0) raw = RawArray(data, info, verbose=True) freqs = np.tile([920, 850], 3) for idx, f in enumerate(freqs): raw.info["chs"][idx]["loc"][9] = f with pytest.raises(ValueError, match='not ordered'): _check_channels_ordered(raw.info, [920, 850]) # Catch if someone doesn't set the info field ch_names = [ 'S1_D1 760', 'S1_D1 850', 'S2_D1 760', 'S2_D1 850', 'S3_D1 760', 'S3_D1 850' ] ch_types = np.repeat("fnirs_cw_amplitude", 6) info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=1.0) raw = RawArray(data, info, verbose=True) with pytest.raises(ValueError, match='missing wavelength information'): _check_channels_ordered(raw.info, [920, 850]) # I have seen data encoded not in alternating frequency, but blocked. ch_names = [ 'S1_D1 760', 'S2_D1 760', 'S3_D1 760', 'S1_D1 850', 'S2_D1 850', 'S3_D1 850' ] ch_types = np.repeat("fnirs_cw_amplitude", 6) info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=1.0) raw = RawArray(data, info, verbose=True) freqs = np.repeat([760, 850], 3) for idx, f in enumerate(freqs): raw.info["chs"][idx]["loc"][9] = f with pytest.raises(ValueError, match='channels not ordered correctly'): _check_channels_ordered(raw.info, [760, 850]) # and this is how you would fix the ordering, then it should pass raw.pick(picks=[0, 3, 1, 4, 2, 5]) _check_channels_ordered(raw.info, [760, 850])
def scalp_coupling_index_windowed(raw, time_window=10, threshold=0.1, l_freq=0.7, h_freq=1.5, l_trans_bandwidth=0.3, h_trans_bandwidth=0.3, verbose=False): """ Compute scalp coupling index for each channel and time window. As described in [1]_ and [2]_. This method provides a metric of data quality along the duration of the measurement. The user can specify the window over which the metric is computed. Parameters ---------- raw : instance of Raw The haemoglobin data. time_window : number The duration of the window over which to calculate the metric. Default is 10 seconds as in PHOEBE paper. threshold : number Values below this are marked as bad and annotated in the raw file. %(l_freq)s %(h_freq)s %(l_trans_bandwidth)s %(h_trans_bandwidth)s %(verbose)s Returns ------- raw : instance of Raw The Raw data. Optionally annotated with bad segments. scores : array (n_nirs, n_windows) Array of peak power values. times : list List of the start and end times of each window used to compute the peak spectral power. References ---------- .. [1] Pollonini L et al., “PHOEBE: a method for real time mapping of optodes-scalp coupling in functional near-infrared spectroscopy” in Biomed. Opt. Express 7, 5104-5119 (2016). .. [2] Hernandez, Samuel Montero, and Luca Pollonini. "NIRSplot: a tool for quality assessment of fNIRS scans." Optics and the Brain. Optical Society of America, 2020. """ raw = raw.copy().load_data() _validate_type(raw, BaseRaw, 'raw') if not len(pick_types(raw.info, fnirs='fnirs_od')): raise RuntimeError('Scalp coupling index ' 'should be run on optical density data.') freqs = np.unique(_channel_frequencies(raw.info)) picks = _check_channels_ordered(raw.info, freqs) filtered_data = filter_data(raw._data, raw.info['sfreq'], l_freq, h_freq, picks=picks, verbose=verbose, l_trans_bandwidth=l_trans_bandwidth, h_trans_bandwidth=h_trans_bandwidth) window_samples = int(np.ceil(time_window * raw.info['sfreq'])) n_windows = int(np.floor(len(raw) / window_samples)) scores = np.zeros((len(picks), n_windows)) times = [] for window in range(n_windows): start_sample = int(window * window_samples) end_sample = start_sample + window_samples end_sample = np.min([end_sample, len(raw) - 1]) t_start = raw.times[start_sample] t_stop = raw.times[end_sample] times.append((t_start, t_stop)) for ii in picks[::2]: c1 = filtered_data[ii][start_sample:end_sample] c2 = filtered_data[ii + 1][start_sample:end_sample] c = np.corrcoef(c1, c2)[0][1] scores[ii, window] = c scores[ii + 1, window] = c if (threshold is not None) & (c < threshold): raw.annotations.append(t_start, time_window, 'BAD_SCI', ch_names=[raw.ch_names[ii:ii + 2]]) return raw, scores, times
def test_fnirs_channel_naming_and_order_readers(fname): """Ensure fNIRS channel checking on standard readers.""" # fNIRS data requires specific channel naming and ordering. # All standard readers should pass tests raw = read_raw_nirx(fname) freqs = np.unique(_channel_frequencies(raw)) assert_array_equal(freqs, [760, 850]) chroma = np.unique(_channel_chromophore(raw)) assert len(chroma) == 0 picks = _check_channels_ordered(raw, freqs) assert len(picks) == len(raw.ch_names) # as all fNIRS only data # Check that dropped channels are detected # For each source detector pair there must be two channels, # removing one should throw an error. raw_dropped = raw.copy().drop_channels(raw.ch_names[4]) with pytest.raises(ValueError, match='not ordered correctly'): _check_channels_ordered(raw_dropped, freqs) # The ordering must match the passed in argument raw_names_reversed = raw.copy().ch_names raw_names_reversed.reverse() raw_reversed = raw.copy().pick_channels(raw_names_reversed, ordered=True) with pytest.raises(ValueError, match='not ordered .* frequencies'): _check_channels_ordered(raw_reversed, freqs) # So if we flip the second argument it should pass again _check_channels_ordered(raw_reversed, [850, 760]) # Check on OD data raw = optical_density(raw) freqs = np.unique(_channel_frequencies(raw)) assert_array_equal(freqs, [760, 850]) chroma = np.unique(_channel_chromophore(raw)) assert len(chroma) == 0 picks = _check_channels_ordered(raw, freqs) assert len(picks) == len(raw.ch_names) # as all fNIRS only data # Check on haemoglobin data raw = beer_lambert_law(raw) freqs = np.unique(_channel_frequencies(raw)) assert len(freqs) == 0 assert len(_channel_chromophore(raw)) == len(raw.ch_names) chroma = np.unique(_channel_chromophore(raw)) assert_array_equal(chroma, ["hbo", "hbr"]) picks = _check_channels_ordered(raw, chroma) assert len(picks) == len(raw.ch_names) with pytest.raises(ValueError, match='not ordered .* chromophore'): _check_channels_ordered(raw, ["hbx", "hbr"])