EDF/BDF Format¶
EDF (European Data Format) and BDF (BioSemi Data Format) are standard formats for storing multichannel biological and physical signals. biosigIO supports both importing from and exporting to these formats.
Format Description¶
EDF Format¶
EDF (European Data Format) is a simple and flexible format for exchange and storage of multichannel biological and physical signals:
- 16-bit resolution (range: ±32767)
- Supports varying sampling rates between channels
- Includes metadata in the header
- Supports annotations and events
BDF Format¶
BDF (BioSemi Data Format) is an extension of EDF that uses 24-bit resolution instead of 16-bit:
- 24-bit resolution (range: ±8,388,607)
- Higher dynamic range for signals with larger amplitude variations
- Otherwise identical to EDF
File Structure¶
Both formats consist of:
- Header Record: Contains metadata about the recording:
- Version information
- Patient and recording information
- Date and time
- Number of channels
- Sampling rates
-
Calibration values
-
Data Records: Contains the actual signal data, organized in blocks.
biosigIO's Approach to EDF/BDF¶
biosigIO can both import from and export to EDF/BDF formats. When exporting, biosigIO automatically determines which format to use based on the dynamic range of the data:
- If the signal's dynamic range is within 16-bit resolution (~90dB), it uses EDF
- If the signal requires greater precision, it uses BDF
This determination is made using: - SVD (Singular Value Decomposition) analysis to determine the effective rank and energy distribution - FFT (Fast Fourier Transform) analysis to evaluate the noise floor and signal-to-noise ratio
Importer Implementation¶
The EDF importer in biosigIO (biosigio.importers.edf) uses the pyedflib package to:
- Read the EDF/BDF file header to extract metadata
- Load the signal data for all channels
- Convert channel information to biosigIO's format
- Read EDF+/BDF+ annotations into the
eventsDataFrame
Tolerant fallback for non-compliant-but-readable files¶
pyedflib's compliance checker rejects some real-world EDF/BDF files outright
even though the data is byte-intact and other tools (MNE-Python) read them
without complaint. The importer recovers three such conditions instead of
discarding the recording:
- a channel with
physical_min == physical_max(a reference electrode in a referential montage, constant by construction; issue #109) - a numeric header field padded with NUL bytes instead of ASCII spaces
- a file correctly marked EDF+D (discontinuous)
When one of these is detected, biosigIO re-reads the file through MNE and
rescales its SI-volt output back to the same native physical units a normal
pyedflib read would have produced, so the recovered values are numerically
identical to what pyedflib would report for the same channel. A channel with
a degenerate physical_min == physical_max has no defined scale, so its output
is the constant physical_min (== physical_max) throughout -- exactly zero in
the common grounded/referential case. Recovering this way requires the meg
extra (MNE); without it, the file still raises the same
CorruptFileError/FileReadError it did before.
A channel MNE auto-detects as a trigger/status channel (by default, one
literally named "Status" or "Trigger") is read with that detection
turned off (stim_channel=False): MNE would otherwise give such a channel
its own uncalibrated, bitmasked treatment unrelated to the header's declared
physical range, which would silently disagree with a normal pyedflib read
of that channel. With detection off, it is calibrated the same way as every
other channel, preserving the same unit-parity guarantee for it too.
A recovered read is flagged in the recording's metadata:
edf_tolerant_read(True) andedf_tolerant_read_reason(one ofdegenerate_physical_range,malformed_numeric_field,discontinuous_datarecords)- each affected channel additionally carries
degenerate_physical_range: True
A genuinely truncated or corrupt file is unaffected by this: only the three
conditions above are treated as recoverable, and a size check runs before any
recovery attempt so a file that is also truncated still raises
CorruptFileError rather than being silently read short.
Scope of a recovered read: all signal-level data is fully populated --
per-channel units, physical/digital min/max, transducer, prefilter, sample
rate, channel type, and EDF+/BDF+ annotations/events. The one thing NOT
populated is the patient/recording free-text metadata a normal read parses
out of the header's patient_id/recording_id fields (patientcode,
gender, birthdate, patient_name, admincode, technician, equipment,
recording_additional): these keys are simply absent on a recovered read,
not defaulted to empty strings, since biosigIO does not re-implement
pyedflib's own sub-parsing of those two free-text fields for the fallback
path.
A file with two on-disk channels sharing the same label is also not covered: MNE renames the second one to keep channel names unique, which the header probe (independent of MNE by design) cannot predict, so this fails loud with a clear error rather than silently pairing the wrong header row with the wrong channel's data.
Exporter Implementation¶
The EDF exporter in biosigIO (biosigio.exporters.edf) also uses pyedflib to:
- Automatically determine whether to use EDF or BDF based on signal characteristics
- Generate appropriate header information
- Scale the signals correctly for storage
- Write the
eventsDataFrame back as EDF+/BDF+ annotations - Create a sidecar
<output>_channels.tsvfile with detailed channel metadata (BIDS-compatible)
Code Example¶
from biosigio import Recording
# Import from EDF file
rec = Recording.from_file('input.edf', importer='edf')
# Print basic information
print(f"Number of channels: {rec.get_n_channels()}")
print(f"Sampling frequency: {rec.get_sampling_frequency()} Hz")
print(f"Recording duration: {rec.get_duration()} seconds")
# Export to EDF/BDF (format selected automatically)
rec.to_edf('output') # Will add .edf or .bdf extension automatically
# Force a specific format
rec.to_edf('output_edf', format='edf') # Forces 16-bit EDF
rec.to_edf('output_bdf', format='bdf') # Forces 24-bit BDF
# Export with verification to ensure data integrity
rec.to_edf('output_verified', verify=True)
Signal Verification¶
When exporting EMG data to EDF/BDF format, you can optionally verify the integrity of the exported data by setting the verify parameter to True:
# Export with verification
verification_results = rec.to_edf('output', verify=True)
# Export with custom verification settings
verification_results = rec.to_edf(
'output',
verify=True,
verify_tolerance=0.001, # Absolute tolerance for signal comparison
verify_channel_map={'EMG1': 'CH1'}, # Custom channel mapping
verify_plot=True # Generate visualization of comparison
)
The verification process:
1. Exports the data to the specified file
2. Immediately reloads the file using the EDF importer
3. Compares the original and reloaded signals using normalization-based metrics
4. Logs a detailed verification report
5. Optionally generates a visual comparison if verify_plot=True
6. Returns the verification results as a dictionary
Notes and Limitations¶
- The BIDS-compatible
<output>_channels.tsvsidecar file includes detailed channel information - EDF+/BDF+ annotations are loaded into the
eventsDataFrame (columnsonset,duration,description) and written back as EDF+/BDF+ annotations on export - When importing from EDF, biosigIO attempts to identify channel types based on labels and signal characteristics
- When exporting to EDF/BDF, biosigIO automatically handles scaling to maximize precision
- EDF/BDF export requires a single sampling rate across all channels; if per-channel rates differ, export raises
ValueError. Resample channels to a common rate first - With
format='auto',to_edf('output')selects either.edf(16-bit) or.bdf(24-bit) based on signal analysis and appends the matching extension - EDF has limitations on channel naming (maximum 16 characters)