14
14
from ..._freesurfer import get_mni_fiducials
15
15
from ...annotations import Annotations
16
16
from ...transforms import _frame_to_str , apply_trans
17
- from ...utils import _check_fname , _import_h5py , fill_doc , logger , verbose , warn
17
+ from ...utils import (
18
+ _check_fname ,
19
+ _import_h5py ,
20
+ _validate_type ,
21
+ fill_doc ,
22
+ logger ,
23
+ verbose ,
24
+ warn ,
25
+ )
18
26
from ..base import BaseRaw
19
27
from ..nirx .nirx import _convert_fnirs_to_head
20
28
21
29
22
30
@fill_doc
23
31
def read_raw_snirf (
24
- fname , optode_frame = "unknown" , preload = False , verbose = None
32
+ fname , optode_frame = "unknown" , * , sfreq = None , preload = False , verbose = None
25
33
) -> "RawSNIRF" :
26
34
"""Reader for a continuous wave SNIRF data.
27
35
@@ -41,6 +49,11 @@ def read_raw_snirf(
41
49
in which case the positions are not modified. If a known coordinate
42
50
frame is provided (head, meg, mri), then the positions are transformed
43
51
in to the Neuromag head coordinate frame (head).
52
+ sfreq : float | None
53
+ The nominal sampling frequency at which the data were acquired. If ``None``,
54
+ will be estimated from the time data in the file.
55
+
56
+ .. versionadded:: 1.10
44
57
%(preload)s
45
58
%(verbose)s
46
59
@@ -54,7 +67,7 @@ def read_raw_snirf(
54
67
--------
55
68
mne.io.Raw : Documentation of attributes and methods of RawSNIRF.
56
69
"""
57
- return RawSNIRF (fname , optode_frame , preload , verbose )
70
+ return RawSNIRF (fname , optode_frame , sfreq = sfreq , preload = preload , verbose = verbose )
58
71
59
72
60
73
def _open (fname ):
@@ -74,6 +87,11 @@ class RawSNIRF(BaseRaw):
74
87
in which case the positions are not modified. If a known coordinate
75
88
frame is provided (head, meg, mri), then the positions are transformed
76
89
in to the Neuromag head coordinate frame (head).
90
+ sfreq : float | None
91
+ The nominal sampling frequency at which the data were acquired. If ``None``,
92
+ will be estimated from the time data in the file.
93
+
94
+ .. versionadded:: 1.10
77
95
%(preload)s
78
96
%(verbose)s
79
97
@@ -83,7 +101,9 @@ class RawSNIRF(BaseRaw):
83
101
"""
84
102
85
103
@verbose
86
- def __init__ (self , fname , optode_frame = "unknown" , preload = False , verbose = None ):
104
+ def __init__ (
105
+ self , fname , optode_frame = "unknown" , * , sfreq = None , preload = False , verbose = None
106
+ ):
87
107
# Must be here due to circular import error
88
108
from ...preprocessing .nirs import _validate_nirs_info
89
109
@@ -120,7 +140,7 @@ def __init__(self, fname, optode_frame="unknown", preload=False, verbose=None):
120
140
121
141
last_samps = dat .get ("/nirs/data1/dataTimeSeries" ).shape [0 ] - 1
122
142
123
- sampling_rate = _extract_sampling_rate (dat )
143
+ sampling_rate = _extract_sampling_rate (dat , sfreq )
124
144
125
145
if sampling_rate == 0 :
126
146
warn ("Unable to extract sample rate from SNIRF file." )
@@ -531,49 +551,48 @@ def _get_lengthunit_scaling(length_unit):
531
551
)
532
552
533
553
534
- def _extract_sampling_rate (dat ):
554
+ def _extract_sampling_rate (dat , user_sfreq ):
535
555
"""Extract the sample rate from the time field."""
536
556
# This is a workaround to provide support for Artinis data.
537
557
# It allows for a 1% variation in the sampling times relative
538
558
# to the average sampling rate of the file.
539
559
MAXIMUM_ALLOWED_SAMPLING_JITTER_PERCENTAGE = 1.0
540
560
561
+ _validate_type (user_sfreq , ("numeric" , None ), "sfreq" )
541
562
time_data = np .array (dat .get ("nirs/data1/time" ))
542
- sampling_rate = 0
543
- if len (time_data ) == 2 :
544
- # specified as onset, samplerate
545
- sampling_rate = 1.0 / (time_data [1 ] - time_data [0 ])
563
+ time_unit = _get_metadata_str (dat , "TimeUnit" )
564
+ time_unit_scaling = _get_timeunit_scaling (time_unit ) # always 1 (s) or 1000 (ms)
565
+ if len (time_data ) == 2 : # special-cased in the snirf standard as (onset, period)
566
+ onset , period = time_data
567
+ file_sfreq = time_unit_scaling / period
546
568
else :
547
- # specified as time points
569
+ onset = time_data [ 0 ]
548
570
periods = np .diff (time_data )
549
- uniq_periods = np .unique (periods .round (decimals = 4 ))
550
- if uniq_periods .size == 1 :
551
- # Uniformly sampled data
552
- sampling_rate = 1.0 / uniq_periods .item ()
553
- else :
554
- # Hopefully uniformly sampled data with some precision issues.
555
- # This is a workaround to provide support for Artinis data.
556
- mean_period = np .mean (periods )
557
- sampling_rate = 1.0 / mean_period
558
- ideal_times = np .linspace (time_data [0 ], time_data [- 1 ], time_data .size )
559
- max_jitter = np .max (np .abs (time_data - ideal_times ))
560
- percent_jitter = 100.0 * max_jitter / mean_period
561
- msg = (
562
- f"Found jitter of { percent_jitter :3f} % in sample times. Sampling "
563
- f"rate has been set to { sampling_rate :1f} ."
571
+ sfreqs = time_unit_scaling / periods
572
+ file_sfreq = sfreqs .mean () # our best estimate, likely including some jitter
573
+ if user_sfreq is not None :
574
+ logger .info (f"Setting sampling frequency to user-supplied value: { user_sfreq } " )
575
+ if not np .allclose (file_sfreq , user_sfreq , rtol = 0.01 , atol = 0 ):
576
+ warn (
577
+ f"User-supplied sampling frequency ({ user_sfreq } Hz) differs by "
578
+ f"{ (user_sfreq - file_sfreq ) / file_sfreq :.1%} from the frequency "
579
+ f"estimated from data in the file ({ file_sfreq } Hz)."
564
580
)
565
- if percent_jitter > MAXIMUM_ALLOWED_SAMPLING_JITTER_PERCENTAGE :
566
- warn (
567
- f"{ msg } Note that MNE-Python does not currently support SNIRF "
568
- "files with non-uniformly-sampled data."
569
- )
570
- else :
571
- logger .info (msg )
572
- time_unit = _get_metadata_str (dat , "TimeUnit" )
573
- time_unit_scaling = _get_timeunit_scaling (time_unit )
574
- sampling_rate *= time_unit_scaling
575
-
576
- return sampling_rate
581
+ sfreq = user_sfreq or file_sfreq # user-passed value overrides value from file
582
+ # estimate jitter
583
+ if len (time_data ) > 2 :
584
+ ideal_times = onset + np .arange (len (time_data )) / sfreq
585
+ max_jitter = np .max (np .abs (time_data - ideal_times ))
586
+ percent_jitter = 100.0 * max_jitter / periods .mean ()
587
+ msg = f"Found jitter of { percent_jitter :3f} % in sample times."
588
+ if percent_jitter > MAXIMUM_ALLOWED_SAMPLING_JITTER_PERCENTAGE :
589
+ warn (
590
+ f"{ msg } Note that MNE-Python does not currently support SNIRF "
591
+ "files with non-uniformly-sampled data."
592
+ )
593
+ else :
594
+ logger .info (msg )
595
+ return sfreq
577
596
578
597
579
598
def _get_metadata_str (dat , field ):
0 commit comments