EDF headers and pseudonymisation¶
yousleep_common.edf reads the header of an EDF or EDF+ recording, reads its
EDF+ annotations, and holds the rule that pseudonymises a header. Every part of
the platform that reads a header uses this package, so a recording is read the
same way wherever it is read.
Reading a header¶
| Function | Reads |
|---|---|
validate_edf_recording(readable, full_file_size) |
The header, validated, and checked against the size of the whole file |
read_edf_recording_header_from_readable(readable) |
The header, validated |
read_edf_raw_headers_from_readable(readable) |
The fixed and signal header fields, unvalidated |
parse_edf_annotations(readable, …) |
The EDF+ annotations of the data records |
Each returns or reads only what it names: no function reads signal samples.
A header is refused if its version is not 0, if it marks an interrupted
recording (EDF+D), if its size does not match its number of signals, or if
a signal's samples per record are not a positive multiple of the record
duration.
The start date. EDF+ writes the date twice: four digits in the recording
field (Startdate 12-OCT-2026) and two in the start date field (12.10.26).
The four-digit date is read first. When it is missing or X, the start date
field is read, with its two-digit year taken as the latest year not in the
future. Two values mean the date is unknown:
| Value | Meaning | Time of day |
|---|---|---|
EDF_UNKNOWN_START_DATE, 1985-01-01 |
EDF+'s marker for an unknown or removed date, 01.01.85 |
Kept and usable |
NO_HEADER_DATE_YEAR, 1900-01-01 |
The header carries no usable date | Not usable |
Reading a recording for an ingest¶
An ingest job reads every recording once, on a runner, in a container with no
network. The container reads the runner's copy, after the pseudonymisation
rule, and writes an IngestDocument (yousleep_common.models.ingest): the
SHA-256 of the header, and the annotations with the platform's labels.
| Function | Does |
|---|---|
read_recording(path, annotation_text=…) |
Returns the document for a file. Without annotation_text, an annotation that matches no label keeps its label, Unknown, but not its text |
header_sha256(data) |
Returns the SHA-256 of the header at the start of data: the fixed header and every signal header |
The platform hashes the header a client registers with header_sha256, and
compares it with the document's. They are equal when the stored file has the
registered header: an upload's header as cleaned before upload, or a
registered file's header after the rule.
Only the header can make a file unreadable: read_recording then raises
NotAnEdfRecording, and the ingest container exits with
NOT_AN_EDF_RECORDING_EXIT (65), the one exit the platform refuses an upload
for. Annotations come from the uploader's file and never fail the read:
- an onset that is not a number from 0 to 30 days is left out;
- a duration that is negative or not a number is 0, and one longer than 30 days is cut to it;
- text longer than 4,096 characters is cut to that length.
The pseudonymisation rule¶
The rule rewrites three fields of the 256-byte fixed header and copies every other byte. A file keeps its length, its signals, its annotations and its time of day.
| Bytes | Field | Becomes |
|---|---|---|
| 8–87 | local patient identification | X X X X |
| 88–167 | local recording identification | Startdate D X X X |
| 168–175 | start date | 01.01.85 if dates are removed; unchanged if kept |
Every field is padded with spaces. D is the header's own dd-MMM-yyyy date
when dates are kept and that date is valid, and X otherwise.
Dis kept only when the recording field's first subfield is exactlyStartdateand its second is a real calendar date. The month is read in any case and written in upper case, as EDF+ requires.- A plain EDF file's free-text recording field gives
D=X. With dates kept, the date stays in the start date field. - A header that is not 256 bytes, or whose version is not
0, is refused. - Bytes outside printable ASCII are not refused: the fields the rule writes are replaced whole, and every other byte is copied.
- Applying the rule twice gives the same bytes as applying it once.
| Function | Does |
|---|---|
pseudonymise_header(fixed_header, keep_dates=…) |
Returns the rewritten 256 bytes |
is_pseudonymised(fixed_header, keep_dates=…) |
Says whether the rule would change nothing |
pseudonymise_file_header(path, keep_dates=…) |
Rewrites a file's header in place, and says whether it changed |
Annotation text in the data records is outside the rule.
Test vectors¶
yousleep_common/edf/pseudonymisation_vectors.json ships with the package.
Each case gives a fixed header as hex, whether dates are kept, and the expected
rewritten header as hex; an expected value of null means the input must be
refused. Every implementation of the rule is tested against this file. The
package's own tests also rewrite whole EDF and EDF+ files and open them in
pyedflib.
yousleep_common/edf/pseudonymisation_corpus.json, in the same format, holds
400 headers generated from a fixed seed by tests/edf/corpus.py:
- real, impossible and lower-case dates;
- whitespace beyond the space;
- bytes outside ASCII;
- free text;
- headers already cleaned;
- wrong versions and lengths.
Its expected outputs are the Python implementation's. The vectors fix the rule case by case; the corpus holds another implementation to this one across the space between those cases. A test fails while the corpus differs from what the rule gives, so it is written again whenever the rule changes.