Skip to content

Commit 50e577e

Browse files
authored
Merge pull request #234 from ppsp-team/feat/fnirs-minor-fixes
fNIRS improvements and minor fixes
2 parents 4d78b2a + 420e4ff commit 50e577e

24 files changed

Lines changed: 2942 additions & 9935 deletions

data/NIRS/slow_breathing.snirf

478 KB
Binary file not shown.

hypyp/fnirs/dyad.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def __init__(self, s1:Recording, s2:Recording, label:str='', is_pseudo:bool=Fals
5151
self.wtcs = None
5252
self.df = None
5353
self.is_pseudo = is_pseudo
54+
self.is_intra = s1 == s2
5455

5556
self.label = label
5657
if self.label == '':
@@ -269,6 +270,7 @@ def compute_wtcs(
269270
only_time_range: Tuple[float,float] | None = None,
270271
bin_seconds: float | None = None,
271272
period_cuts: List[float] | None = None,
273+
frequency_cuts: List[float] | None = None,
272274
verbose: bool = False,
273275
with_intra: bool = True,
274276
downsample: int | None = None,
@@ -284,7 +286,8 @@ def compute_wtcs(
284286
Defaults to None, which means all channels.
285287
only_time_range (Tuple[float,float] | None, optional): compute only a portion of the signal, defined as time range tuple (start, stop). Defaults to None.
286288
bin_seconds (float | None, optional): split the resulting WTC in time bins for balancing weights. Defaults to None.
287-
period_cuts (List[float] | None, optional): split the resulting WTC in period/frequency bins for balancing weights and finer analysis. Defaults to None.
289+
period_cuts (List[float] | None, optional): split the resulting WTC in period/frequency bins for balancing weights and finer analysis. Use this OR frequency_cuts, not both. Defaults to None.
290+
frequency_cuts (List[float] | None, optional): split the resulting WTC in period/frequency bins for balancing weights and finer analysis. Use this OR period_cuts, not both. Defaults to None.
288291
verbose (bool, optional): verbose flag. Defaults to False.
289292
with_intra (bool, optional): compute intra-subject as well. Defaults to False.
290293
downsample (int | None, optional): downsample in time the resulting WTC. Useful to save memory space and faster display. Defaults to None.
@@ -296,23 +299,35 @@ def compute_wtcs(
296299
if wavelet is None:
297300
wavelet = ComplexMorletWavelet()
298301

302+
if period_cuts is not None and frequency_cuts is not None:
303+
raise RuntimeError('Cannot specify both period_cuts and frequency_cuts')
304+
299305
self.wtcs = []
300306

301307
pairs = self.get_pairs(self.s1, self.s2, ch_match=ch_match, is_pseudo=self.is_pseudo)
302308

303-
for pair in pairs:
304-
if verbose:
305-
print(f'Running Wavelet Coherence for dyad "{self.label}" on pair "{pair.label}"')
306-
if only_time_range is not None:
307-
pair = pair.sub(only_time_range)
308-
wtc = wavelet.wtc(pair, bin_seconds=bin_seconds, period_cuts=period_cuts)
309-
if downsample is not None:
310-
wtc.downsample_in_time(downsample)
309+
if self.is_intra:
310+
# Force with_intra when subject 1 is the same as subject 2
311+
with_intra = True
312+
else:
313+
for pair in pairs:
314+
if verbose:
315+
print(f'Running Wavelet Coherence for dyad "{self.label}" on pair "{pair.label}"')
316+
if only_time_range is not None:
317+
pair = pair.sub(only_time_range)
318+
wtc = wavelet.wtc(pair, bin_seconds=bin_seconds, period_cuts=period_cuts, frequency_cuts=frequency_cuts)
319+
if downsample is not None:
320+
wtc.downsample_in_time(downsample)
311321

312-
self.wtcs.append(wtc)
322+
self.wtcs.append(wtc)
313323

314324
if with_intra:
315-
for i, recording in enumerate([self.s1, self.s2]):
325+
if self.is_intra:
326+
subjects = [self.s1]
327+
else:
328+
subjects = [self.s1, self.s2]
329+
330+
for i, recording in enumerate(subjects):
316331
recording.intra_wtcs = []
317332
# if we have different channels for each recording, the intra wtc should use only the ones for this subject
318333
if isinstance(ch_match, tuple):
@@ -327,7 +342,7 @@ def compute_wtcs(
327342
print(f'Running Wavelet Coherence intra-subject "{recording.subject_label}" on pair "{pair.label}"')
328343
if only_time_range is not None:
329344
pair = pair.sub(only_time_range)
330-
wtc = wavelet.wtc(pair, bin_seconds=bin_seconds, period_cuts=period_cuts)
345+
wtc = wavelet.wtc(pair, bin_seconds=bin_seconds, period_cuts=period_cuts, frequency_cuts=frequency_cuts)
331346
if downsample is not None:
332347
wtc.downsample_in_time(downsample)
333348
recording.intra_wtcs.append(wtc)

hypyp/fnirs/preprocessor/implementations/mne_preprocessor_as_is.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def read_file(self, path:str, verbose:bool=False):
2929
if DataBrowser.is_path_snirf(path):
3030
return mne.io.read_raw_snirf(path, preload=True, verbose=verbose)
3131

32-
return None
32+
raise ValueError(f"No reader for file {path}")
3333

3434
def run(self, raw: mne.io.Raw, verbose: bool = False) -> list[MneStep]:
3535
if verbose:

hypyp/fnirs/preprocessor/implementations/mne_preprocessor_raw_to_haemo.py

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,46 +26,38 @@ class MnePreprocessorRawToHaemo(MnePreprocessorAsIs):
2626
already preprocessed and will be returned as-is.
2727
2828
"""
29-
def __init__(self):
29+
def __init__(self, ppf=6.0, hp_filter=0.02, lp_filter=0.7, quality_sci_threshold=0.1):
3030
super().__init__()
31+
self.ppf = ppf
32+
self.hp_filter = hp_filter
33+
self.lp_filter = lp_filter
34+
self.quality_sci_threshold = quality_sci_threshold
3135

3236
def run(self, raw:mne.io.Raw, verbose:bool=False) -> list[MneStep]:
37+
# If have hbo or hbr, it means it is already in concentration
38+
if len(mne.pick_types(raw.info, fnirs=['hbo', 'hbr'])) > 0:
39+
raise ValueError('Loaded data seems to already be in concentrations. Use MnePreprocessorAsIs instead.')
40+
3341
if verbose:
3442
print('Using MnePreprocessorRawToHaemo, converting fNIRS raw data to haemoglobin concentrations')
3543

3644
steps = []
3745
steps.append(MneStep(raw, PREPROCESS_STEP_BASE_KEY, PREPROCESS_STEP_BASE_DESC))
3846

39-
haemo_picks = mne.pick_types(raw.info, fnirs=['hbo', 'hbr'])
40-
41-
# If we have haemo_picks, it means it is already preprocessed
42-
# TODO: this code flow if confusing
43-
if len(haemo_picks) > 0:
44-
if verbose:
45-
print('Data loaded using MNE seems to already be converted to haemoglobin concentrations')
46-
47-
steps.append(MneStep(
48-
raw.copy().pick(haemo_picks),
49-
PREPROCESS_STEP_HAEMO_FILTERED_KEY,
50-
PREPROCESS_STEP_HAEMO_FILTERED_DESC
51-
))
52-
return steps
53-
5447
raw_od = mne.preprocessing.nirs.optical_density(raw)
5548
steps.append(MneStep(raw_od, PREPROCESS_STEP_OD_KEY, PREPROCESS_STEP_OD_DESC))
5649

5750
quality_sci = mne.preprocessing.nirs.scalp_coupling_index(raw_od)
58-
raw_od.info['bads'] = list(compress(raw_od.ch_names, quality_sci < 0.1))
51+
raw_od.info['bads'] = list(compress(raw_od.ch_names, quality_sci < self.quality_sci_threshold))
5952
picks = mne.pick_types(raw_od.info, fnirs=True, exclude='bads')
6053
raw_od_clean = raw_od.copy().pick(picks)
6154
steps.append(MneStep(raw_od_clean, PREPROCESS_STEP_OD_CLEAN_KEY, PREPROCESS_STEP_OD_CLEAN_DESC))
6255

63-
# TODO: should set Partial Pathlength Factor (PPF) depending on age.
64-
# See https://doi.org/10.1117/1.JBO.18.10.105004, Table 1
65-
raw_haemo: mne.io.Raw = mne.preprocessing.nirs.beer_lambert_law(raw_od_clean)
56+
# For partial pathlength, see https://doi.org/10.1117/1.JBO.18.10.105004, Table 1
57+
raw_haemo: mne.io.Raw = mne.preprocessing.nirs.beer_lambert_law(raw_od_clean.copy(), ppf=self.ppf)
6658
steps.append(MneStep(raw_haemo, PREPROCESS_STEP_HAEMO_KEY, PREPROCESS_STEP_HAEMO_DESC))
67-
raw_haemo_filtered = raw_haemo.copy().filter(0.02, 0.7, verbose=verbose)
6859

60+
raw_haemo_filtered = raw_haemo.copy().filter(self.hp_filter, self.lp_filter, verbose=verbose, method='iir')
6961
steps.append(MneStep(raw_haemo_filtered, PREPROCESS_STEP_HAEMO_FILTERED_KEY, PREPROCESS_STEP_HAEMO_FILTERED_DESC))
7062

7163
return steps

0 commit comments

Comments
 (0)