-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
[MRG] Enable export of EDF files #9643
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Hi @Teuniz and mne-python maintainers I took a stab at implementing export functionality, but am still not getting lossless conversion when I re-read the data. I think perhaps there is an issue with digital max/min and physical max/min? The main part of the file is: https://github.com/mne-tools/mne-python/blob/aa85bae98415f12da1710de25955c8a33352ffa0/mne/export/_edf.py The Jupiter notebooks are temporary to show the process I am taking. |
I had a quick look at the code but everything looks fine to me. |
|
In addition to what @Teuniz has already said, EDF files also have a record size, which I believe is 1s by default. Therefore, if your signal is not an integer multiple of the record size, you will have additional samples at the end of the file. AFAIK it is not possible to change this value in Two comments regarding your implementation:
|
Nope, there isn't. Sample data needs to be written per datarecord. |
OK, so unlike https://pyedflib.readthedocs.io/en/latest/ref/edfwriter.html#pyedflib.EdfWriter.writeSamples then. |
Yes, the datarecord duration has a default value of 1 second but can be changed using "setDataRecordDuration()". |
Is there any caveat setting it to a value of |
Good question and yes there are. edit: a datarecord duration longer than 1 second is possible. |
|
In my experience, it's better to set the datarecord duration to one of the following values, if possible: |
|
Thanks @Teuniz, these are some important insights. I'd go for a simple approach then, namely using a 1s data record duration and filling any remaining samples with zero (of course if that is the case I'd issue a warning to inform the user what happened). |
@cbrnr I'm not very familiar with the physical min/max stuff. My understanding is that these are the "ranges" at which the actual analog physical values (i.e. voltage) were capable of having. I followed @Teuniz 's example test files in EDFlib-Python. Is there a good way I could proceed with setting these based on the data? Is there an issue w/ hardcoding it in this way? |
|
See how I do it in the link above. |
This is not as intended by the EDF format. A similar issue was going on here: sccn/eeglab#246 |
|
@Teuniz Any thoughts on how to test before/after writing data and improving the matching of the exported data? I'm only getting matches up to 1-2 decimal places after exporting the data. Wouldn't/shouldn't the resolution improve if I use a smaller range (physical min/max) based on your calculation above? |
|
I'd still implement a safety check to make sure signal maxima/minima are not exceeding the hard-coded values. |
|
@rob-luke it's a good question but a hard question for software engineering.... I think this question is targeted for the downstream projects we rely on for exporting. I have been reluctant |
It complains because it expects an integer value but physical max/min can also be a real number. So, if Fieldtrip, or any other software for that matter, can only handle a subset of BDF (e.g. all channels must Rationale: |
|
Thanks for the information @Teuniz and @agramfort
I think this is an unnecessarily harsh criticism. That software is very good. The package does exactly what it claims "to read biosemi BDF files", and I have found it to be extremely robust, I've read literally hundreds of files with it from different labs. Which brings me to my second point... I assumed (and I guess others will to), that if you can read a file from a biosemi device in the biosemi data format, then you can read any bdf. Most people don't care to read the file format specification, and based on the file extension name this behaviour isn't intuitive. I suggest to add a very prominent note to the exporter highlighting what you've told me. That despite the name being BIOSEMI data format, that BDF is a more encompassing specification and that these files may not be compatible with other software that is able to read biosemi data files. |
|
Perhaps in the physical min max auto setting we just round up to the nearest integer and this would solve this issue? |
It's not the only issue. There can be an issue with some software that expect equal samplerates for all signals, |
|
I would simply disable BDF export. This file format is likely not meant to be an extension for EDF, but rather something that Biosemi created to store their data with more precision. If someone needs a better format than EDF (i.e. not I don't want this thread turn into a file format discussion. Let's just acknowledge that there are several widely used file formats around that we should support. We already support reading many popular formats, but we don't have to do that for exporting. I'd restrict MNE-Python export support to the most popular formats only, which IMO are EDF, BrainVision, and EEGLAB. |
|
Alright, removing BDF support and just re-pushing. |
Co-authored-by: Alexandre Gramfort <alexandre.gramfort@m4x.org>
agramfort
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cbrnr feel free to MRG if happy
sappelhoff
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice work @adam2392 and all the reviewers
Co-authored-by: Stefan Appelhoff <stefan.appelhoff@mailbox.org>
cbrnr
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice work @adam2392!
mne/export/_edf.py
Outdated
| raise RuntimeError(f"Setting Patient Birth Date to {birthday} " | ||
| f"returned an error") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we be more specific here, e.g. mention the error or at least offer a hint what could have gone wrong?
| raise RuntimeError(f"Setting Patient Birth Date to {birthday} " | |
| f"returned an error") | |
| raise RuntimeError(f"Setting patient birth date to {birthday} " | |
| f"returned an error") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See: #9643 (comment)
I don't think this error would ever actually occur, unless someone hacked MNE to make the birthdate non-compliant in the first place. MNE has checks for these types of metadata already in place in Raw.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So why are we re-raising the error then if we cannot be more specific? I would not check for this error if it is already checked elsewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the issue is that EDFLib-Python doesn't actually raise an error. It will just return 0 for error and 1 for success.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming we meet the specifications of EDF when setting up our data structures, then everything should work. I guess, I meant to say, these error checks are there to make sure that at least the user is aware that there was some buggy implementation change on our end that strayed away from EDF, and thus export EDF stopped working.
Else just returning 0 would not result in a Python error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lmk wyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it would be interesting to see what happens. Does the EDF file still get created?
In any case, why don't you use _try_to_set_value() in these cases? This already raises an error if the return value is not equal to zero.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_try_to_set_value only works on one argument functions. I wasn't sure how to generalize that function to make it for these 2 cases :/.
I'll add some tests for faulty annotations/birthdates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can test birth date and measurement date with non-compliant EDF metadata, but Annotations implicitly only has errors that are already checked and controlled for in Raw, so I can't actually even create an error:
# add a bad annotation
annots = Annotations(onset=-1, duration=0, description='test')
raw = RawArray(data, info)
raw.set_annotations(annots)
with pytest.raises(RuntimeError, match='writeAnnotation()'):
raw.export(temp_fname)
^ won't even work cuz the annotation is dropped.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated the test file to reflect these two additional tests
mne/export/_edf.py
Outdated
| raise RuntimeError(f"Setting Start Date Time {meas_date} " | ||
| f"returned an error") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, can we be more specific?
| raise RuntimeError(f"Setting Start Date Time {meas_date} " | |
| f"returned an error") | |
| raise RuntimeError(f"Setting start date time {meas_date} " | |
| f"returned an error") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See: #9643 (comment)
I don't think this error would ever actually occur, unless someone hacked MNE. MNE has checks for these types of metadata already in place in Raw. If any of these errors ever did occur, it would be some weird runtime bug I presume.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here.
Co-authored-by: Clemens Brunner <clemens.brunner@gmail.com>
|
Thanks @adam2392! |
Reference issue
Closes: #9566
What does this implement/fix?
Enables exporting of EDF+
and BDF files,which support annotations.Note that the montage (i.e. info['dig']) is technically lost upon export.
Additional information
If you look at the Jupyter notebook, then I'm not getting lossless conversion. I think I am perhaps setting the physical max/min and digital max/min incorrectly.