77
88import numpy as np
99
10- from . .annotations import _sync_onset
11- from . .utils import _check_edfio_installed , warn
10+ from mne .annotations import _sync_onset
11+ from mne .utils import _check_edfio_installed , warn
1212
1313_check_edfio_installed ()
14- from edfio import Edf , EdfAnnotation , EdfSignal , Patient , Recording # noqa: E402
14+ from edfio import ( # noqa: E402
15+ Bdf ,
16+ BdfSignal ,
17+ Edf ,
18+ EdfAnnotation ,
19+ EdfSignal ,
20+ Patient ,
21+ Recording ,
22+ )
1523
1624
1725# copied from edfio (Apache license)
@@ -29,44 +37,61 @@ def _round_float_to_8_characters(
2937 return round_func (value * factor ) / factor
3038
3139
32- def _export_raw (fname , raw , physical_range , add_ch_type ):
33- """Export Raw objects to EDF files.
40+ def _export_raw_edf_bdf (fname , raw , physical_range , add_ch_type , file_format ):
41+ """Export Raw objects to EDF/BDF files.
3442
43+ Parameters
44+ ----------
45+ fname : str
46+ Output file name.
47+ raw : instance of Raw
48+ The raw instance to export.
49+ physical_range : str or tuple
50+ Physical range setting.
51+ add_ch_type : bool
52+ Whether to add channel type to signal label.
53+ file_format : str
54+ File format ("EDF" or "BDF").
55+
56+ Notes
57+ -----
3558 TODO: if in future the Info object supports transducer or technician information,
3659 allow writing those here.
3760 """
38- # get voltage-based data in uV
3961 units = dict (
4062 eeg = "uV" , ecog = "uV" , seeg = "uV" , eog = "uV" , ecg = "uV" , emg = "uV" , bio = "uV" , dbs = "uV"
4163 )
4264
43- digital_min , digital_max = - 32767 , 32767
44- annotations = []
45-
46- # load data first
47- raw .load_data ()
65+ if file_format == "EDF" :
66+ digital_min , digital_max = - 32767 , 32767 # 16-bit
67+ signal_class = EdfSignal
68+ writer_class = Edf
69+ else : # BDF
70+ digital_min , digital_max = - 8388607 , 8388607 # 24-bit
71+ signal_class = BdfSignal
72+ writer_class = Bdf
4873
4974 ch_types = np .array (raw .get_channel_types ())
50- n_times = raw .n_times
5175
52- # get the entire dataset in uV
76+ # load and prepare data
77+ raw .load_data ()
5378 data = raw .get_data (units = units )
54-
55- # Sampling frequency in EDF only supports integers, so to allow for float sampling
56- # rates from Raw, we adjust the output sampling rate for all channels and the data
57- # record duration.
5879 sfreq = raw .info ["sfreq" ]
80+ pad_annotations = []
81+
82+ # Sampling frequency in EDF/BDF only supports integers, so to allow for float
83+ # sampling rates from Raw, we adjust the output sampling rate for all channels and
84+ # the data record duration.
5985 if float (sfreq ).is_integer ():
6086 out_sfreq = int (sfreq )
6187 data_record_duration = None
6288 # make non-integer second durations work
63- if (pad_width := int (np .ceil (n_times / sfreq ) * sfreq - n_times )) > 0 :
89+ if (pad_width := int (np .ceil (raw . n_times / sfreq ) * sfreq - raw . n_times )) > 0 :
6490 warn (
65- "EDF format requires equal-length data blocks, so "
91+ f" { file_format } format requires equal-length data blocks, so "
6692 f"{ pad_width / sfreq :.3g} seconds of edge values were appended to all "
6793 "channels when writing the final block."
6894 )
69- orig_shape = data .shape
7095 data = np .pad (
7196 data ,
7297 (
@@ -75,10 +100,8 @@ def _export_raw(fname, raw, physical_range, add_ch_type):
75100 ),
76101 "edge" ,
77102 )
78- assert data .shape [0 ] == orig_shape [0 ]
79- assert data .shape [1 ] > orig_shape [1 ]
80103
81- annotations .append (
104+ pad_annotations .append (
82105 EdfAnnotation (
83106 raw .times [- 1 ] + 1 / sfreq , pad_width / sfreq , "BAD_ACQ_SKIP"
84107 )
@@ -89,18 +112,19 @@ def _export_raw(fname, raw, physical_range, add_ch_type):
89112 )
90113 out_sfreq = np .floor (sfreq ) / data_record_duration
91114 warn (
92- f"Data has a non-integer sampling rate of { sfreq } ; writing to EDF format "
93- " may cause a small change to sample times."
115+ f"Data has a non-integer sampling rate of { sfreq } ; writing to "
116+ f" { file_format } format may cause a small change to sample times."
94117 )
95118
96- # get any filter information applied to the data
119+ # extract filter information
97120 lowpass = raw .info ["lowpass" ]
98121 highpass = raw .info ["highpass" ]
99122 linefreq = raw .info ["line_freq" ]
100123 filter_str_info = f"HP:{ highpass } Hz LP:{ lowpass } Hz"
101124 if linefreq is not None :
102- filter_str_info += " N:{linefreq}Hz"
125+ filter_str_info += f " N:{ linefreq } Hz"
103126
127+ # compute physical range
104128 if physical_range == "auto" :
105129 # get max and min for each channel type data
106130 ch_types_phys_max = dict ()
@@ -136,15 +160,17 @@ def _export_raw(fname, raw, physical_range, add_ch_type):
136160 )
137161 data = np .clip (data , pmin , pmax )
138162 prange = pmin , pmax
163+
164+ # create signals
139165 signals = []
140166 for idx , ch in enumerate (raw .ch_names ):
141167 ch_type = ch_types [idx ]
142168 signal_label = f"{ ch_type .upper ()} { ch } " if add_ch_type else ch
143169 if len (signal_label ) > 16 :
144170 raise RuntimeError (
145171 f"Signal label for { ch } ({ ch_type } ) is longer than 16 characters, which"
146- " is not supported by the EDF standard. Please shorten the channel name "
147- " before exporting to EDF ."
172+ f " is not supported by the { file_format } standard. Please shorten the "
173+ f"channel name before exporting to { file_format } ."
148174 )
149175
150176 if physical_range == "auto" : # per channel type
@@ -155,7 +181,7 @@ def _export_raw(fname, raw, physical_range, add_ch_type):
155181 prange = pmin , pmax
156182
157183 signals .append (
158- EdfSignal (
184+ signal_class (
159185 data [idx ],
160186 out_sfreq ,
161187 label = signal_label ,
@@ -167,7 +193,7 @@ def _export_raw(fname, raw, physical_range, add_ch_type):
167193 )
168194 )
169195
170- # set patient info
196+ # create patient info
171197 subj_info = raw .info .get ("subject_info" )
172198 if subj_info is not None :
173199 # get the full name of subject if available
@@ -197,7 +223,7 @@ def _export_raw(fname, raw, physical_range, add_ch_type):
197223 else :
198224 patient = None
199225
200- # set measurement date
226+ # create recording info
201227 if (meas_date := raw .info ["meas_date" ]) is not None :
202228 startdate = dt .date (meas_date .year , meas_date .month , meas_date .day )
203229 starttime = dt .time (
@@ -214,9 +240,11 @@ def _export_raw(fname, raw, physical_range, add_ch_type):
214240 else :
215241 recording = Recording (startdate = startdate )
216242
243+ # create annotations
244+ annotations = []
217245 for desc , onset , duration , ch_names in zip (
218246 raw .annotations .description ,
219- # subtract raw.first_time because EDF marks events starting from the first
247+ # subtract raw.first_time because EDF/BDF marks events starting from the first
220248 # available data point and ignores raw.first_time
221249 _sync_onset (raw , raw .annotations .onset , inverse = False ),
222250 raw .annotations .duration ,
@@ -230,11 +258,24 @@ def _export_raw(fname, raw, physical_range, add_ch_type):
230258 else :
231259 annotations .append (EdfAnnotation (onset , duration , desc ))
232260
233- Edf (
261+ annotations .extend (pad_annotations )
262+
263+ # write to file
264+ writer_class (
234265 signals = signals ,
235266 patient = patient ,
236267 recording = recording ,
237268 starttime = starttime ,
238269 data_record_duration = data_record_duration ,
239270 annotations = annotations ,
240271 ).write (fname )
272+
273+
274+ def _export_raw_edf (fname , raw , physical_range , add_ch_type ):
275+ """Export Raw object to EDF."""
276+ _export_raw_edf_bdf (fname , raw , physical_range , add_ch_type , file_format = "EDF" )
277+
278+
279+ def _export_raw_bdf (fname , raw , physical_range , add_ch_type ):
280+ """Export Raw object to BDF."""
281+ _export_raw_edf_bdf (fname , raw , physical_range , add_ch_type , file_format = "BDF" )
0 commit comments