Python library for reading and writing EDF+/BDF+ files used for storing biomedical signal data
—
Direct access to underlying C library functions for maximum performance and fine-grained control over EDF/BDF file operations and data handling. These functions provide the foundation for the high-level classes and enable custom implementations with minimal overhead.
Direct interface to the underlying C-based EDF reader with minimal Python overhead.
class CyEdfReader:
def __init__(self, file_name: str, annotations_mode: int = READ_ANNOTATIONS,
check_file_size: int = CHECK_FILE_SIZE):
"""
Initialize low-level EDF reader.
Parameters:
- file_name: str, path to EDF/BDF file
- annotations_mode: int, annotation reading mode
- check_file_size: int, file size checking mode
"""
def open(self, file_name: str, annotations_mode: int = READ_ANNOTATIONS,
check_file_size: int = CHECK_FILE_SIZE) -> bool:
"""
Open EDF file for reading.
Parameters:
- file_name: str, path to file
- annotations_mode: int, annotation mode
- check_file_size: int, file size check mode
Returns:
bool: True if successful
"""
def _close(self):
"""Close file and release resources."""
def read_digital_signal(self, signalnum: int, start: int, n: int, sigbuf: np.ndarray):
"""
Read digital signal data directly into buffer.
Parameters:
- signalnum: int, signal number
- start: int, starting sample
- n: int, number of samples
- sigbuf: numpy.ndarray, output buffer (pre-allocated)
"""
def readsignal(self, signalnum: int, start: int, n: int, sigbuf: np.ndarray):
"""
Read physical signal data directly into buffer.
Parameters:
- signalnum: int, signal number
- start: int, starting sample
- n: int, number of samples
- sigbuf: numpy.ndarray, output buffer (pre-allocated)
"""
def read_annotation(self) -> List[List[str]]:
"""
Read annotations as list of string lists.
Returns:
List[List[str]]: Annotation data
"""
def load_datarecord(self, db: np.ndarray, n: int = 1):
"""
Load data records directly.
Parameters:
- db: numpy.ndarray, data buffer
- n: int, number of records to load
"""Direct access to file metadata through properties (read-only).
# File properties
@property
def file_name(self) -> str: ...
@property
def handle(self) -> int: ...
@property
def datarecords_in_file(self) -> int: ...
@property
def signals_in_file(self) -> int: ...
@property
def file_duration(self) -> int: ...
@property
def filetype(self) -> int: ...
@property
def datarecord_duration(self) -> int: ...
@property
def annotations_in_file(self) -> int: ...
# Patient properties
@property
def patient(self) -> str: ...
@property
def patientcode(self) -> str: ...
@property
def sex(self) -> int: ...
@property
def gender(self) -> int: ...
@property
def birthdate(self) -> str: ...
@property
def patientname(self) -> str: ...
@property
def patient_additional(self) -> str: ...
# Recording properties
@property
def recording(self) -> str: ...
@property
def startdate_year(self) -> int: ...
@property
def startdate_month(self) -> int: ...
@property
def startdate_day(self) -> int: ...
@property
def starttime_hour(self) -> int: ...
@property
def starttime_minute(self) -> int: ...
@property
def starttime_second(self) -> int: ...
@property
def starttime_subsecond(self) -> int: ...
@property
def admincode(self) -> str: ...
@property
def technician(self) -> str: ...
@property
def equipment(self) -> str: ...
@property
def recording_additional(self) -> str: ...Access signal-specific information using method calls with signal index.
def signal_label(self, signalnum: int) -> str:
"""Get signal label."""
def samples_in_file(self, signalnum: int) -> int:
"""Get total samples in file for signal."""
def samples_in_datarecord(self, signalnum: int) -> int:
"""Get samples per data record for signal."""
def physical_dimension(self, signalnum: int) -> str:
"""Get physical dimension (units) for signal."""
def physical_max(self, signalnum: int) -> float:
"""Get physical maximum value for signal."""
def physical_min(self, signalnum: int) -> float:
"""Get physical minimum value for signal."""
def digital_max(self, signalnum: int) -> int:
"""Get digital maximum value for signal."""
def digital_min(self, signalnum: int) -> int:
"""Get digital minimum value for signal."""
def prefilter(self, signalnum: int) -> str:
"""Get prefilter description for signal."""
def transducer(self, signalnum: int) -> str:
"""Get transducer description for signal."""
def samplefrequency(self, signalnum: int) -> float:
"""Get sampling frequency for signal."""
def smp_per_record(self, signalnum: int) -> int:
"""Get samples per record for signal."""Usage example:
import pyedflib
import numpy as np
# Open file with low-level reader
reader = pyedflib.CyEdfReader('data.edf')
# Get file info
print(f"File: {reader.file_name}")
print(f"Signals: {reader.signals_in_file}")
print(f"Duration: {reader.file_duration} seconds")
print(f"Patient: {reader.patientname}")
# Read signal data directly into pre-allocated buffer
signal_samples = reader.samples_in_file(0)
buffer = np.zeros(signal_samples, dtype=np.float64)
reader.readsignal(0, 0, signal_samples, buffer)
# Get signal properties
label = reader.signal_label(0)
sfreq = reader.samplefrequency(0)
phys_min = reader.physical_min(0)
phys_max = reader.physical_max(0)
print(f"Signal 0: {label}, {sfreq} Hz, range [{phys_min}, {phys_max}]")
reader._close()Low-level file management functions for writing operations.
def lib_version() -> str:
"""
Get EDFlib version.
Returns:
str: Library version string
"""
def open_file_writeonly(path: str, filetype: int, number_of_signals: int) -> int:
"""
Open file for writing.
Parameters:
- path: str, file path
- filetype: int, file type constant
- number_of_signals: int, number of signal channels
Returns:
int: File handle (>0 on success, <0 on error)
"""
def close_file(handle: int) -> int:
"""
Close file by handle.
Parameters:
- handle: int, file handle from open_file_writeonly
Returns:
int: 0 on success, error code on failure
"""
def get_number_of_open_files() -> int:
"""
Get number of currently open files.
Returns:
int: Number of open files
"""
def get_handle(file_number: int) -> int:
"""
Get handle for file number.
Parameters:
- file_number: int, file number
Returns:
int: File handle
"""
def is_file_used(path: str) -> bool:
"""
Check if file is currently in use.
Parameters:
- path: str, file path
Returns:
bool: True if file is in use
"""Low-level functions for reading signal data with direct buffer access.
def read_int_samples(handle: int, edfsignal: int, n: int, buf: np.ndarray) -> int:
"""
Read integer samples directly into buffer.
Parameters:
- handle: int, file handle
- edfsignal: int, signal number
- n: int, number of samples to read
- buf: numpy.ndarray, pre-allocated buffer
Returns:
int: Number of samples read
"""
def read_physical_samples(handle: int, edfsignal: int, n: int, buf: np.ndarray) -> int:
"""
Read physical samples directly into buffer.
Parameters:
- handle: int, file handle
- edfsignal: int, signal number
- n: int, number of samples to read
- buf: numpy.ndarray, pre-allocated buffer
Returns:
int: Number of samples read
"""
def get_annotation(handle: int, n: int, edf_annotation: EdfAnnotation) -> int:
"""
Get annotation by index.
Parameters:
- handle: int, file handle
- n: int, annotation index
- edf_annotation: EdfAnnotation, annotation object to populate
Returns:
int: 0 on success, error code on failure
"""Low-level functions for writing signal data with different data types and block modes.
def write_digital_samples(handle: int, buf: np.ndarray) -> int:
"""
Write digital samples to file.
Parameters:
- handle: int, file handle
- buf: numpy.ndarray, digital sample data
Returns:
int: Number of samples written
"""
def write_digital_short_samples(handle: int, buf: np.ndarray) -> int:
"""
Write digital short samples to file.
Parameters:
- handle: int, file handle
- buf: numpy.ndarray, digital short sample data
Returns:
int: Number of samples written
"""
def write_physical_samples(handle: int, buf: np.ndarray) -> int:
"""
Write physical samples to file.
Parameters:
- handle: int, file handle
- buf: numpy.ndarray, physical sample data
Returns:
int: Number of samples written
"""
def blockwrite_digital_samples(handle: int, buf: np.ndarray) -> int:
"""
Block write digital samples.
Parameters:
- handle: int, file handle
- buf: numpy.ndarray, digital sample data
Returns:
int: Number of samples written
"""
def blockwrite_digital_short_samples(handle: int, buf: np.ndarray) -> int:
"""
Block write digital short samples.
Parameters:
- handle: int, file handle
- buf: numpy.ndarray, digital short sample data
Returns:
int: Number of samples written
"""
def blockwrite_physical_samples(handle: int, buf: np.ndarray) -> int:
"""
Block write physical samples.
Parameters:
- handle: int, file handle
- buf: numpy.ndarray, physical sample data
Returns:
int: Number of samples written
"""Low-level functions for configuring file-level header information.
def set_patientcode(handle: int, patientcode: Union[str, bytes]) -> int:
"""Set patient identification code."""
def set_patientname(handle: int, name: Union[str, bytes]) -> int:
"""Set patient name."""
def set_patient_additional(handle: int, patient_additional: Union[str, bytes]) -> int:
"""Set additional patient information."""
def set_admincode(handle: int, admincode: Union[str, bytes]) -> int:
"""Set administration code."""
def set_technician(handle: int, technician: Union[str, bytes]) -> int:
"""Set technician name."""
def set_equipment(handle: int, equipment: Union[str, bytes]) -> int:
"""Set equipment description."""
def set_recording_additional(handle: int, recording_additional: Union[str, bytes]) -> int:
"""Set additional recording information."""
def set_birthdate(handle: int, birthdate_year: int, birthdate_month: int,
birthdate_day: int) -> int:
"""
Set patient birthdate.
Parameters:
- handle: int, file handle
- birthdate_year: int, birth year
- birthdate_month: int, birth month (1-12)
- birthdate_day: int, birth day (1-31)
Returns:
int: 0 on success, error code on failure
"""
def set_sex(handle: int, sex: Optional[int]) -> int:
"""
Set patient sex.
Parameters:
- handle: int, file handle
- sex: int or None, sex code (0=female, 1=male)
Returns:
int: 0 on success, error code on failure
"""
def set_startdatetime(handle: int, startdate_year: int, startdate_month: int,
startdate_day: int, starttime_hour: int, starttime_minute: int,
starttime_second: int) -> int:
"""
Set recording start date and time.
Parameters:
- handle: int, file handle
- startdate_year: int, start year
- startdate_month: int, start month (1-12)
- startdate_day: int, start day (1-31)
- starttime_hour: int, start hour (0-23)
- starttime_minute: int, start minute (0-59)
- starttime_second: int, start second (0-59)
Returns:
int: 0 on success, error code on failure
"""
def set_starttime_subsecond(handle: int, subsecond: int) -> int:
"""
Set subsecond precision for start time.
Parameters:
- handle: int, file handle
- subsecond: int, subsecond value
Returns:
int: 0 on success, error code on failure
"""
def set_datarecord_duration(handle: int, duration: Union[int, float]) -> int:
"""
Set data record duration.
Parameters:
- handle: int, file handle
- duration: int or float, record duration in seconds
Returns:
int: 0 on success, error code on failure
"""
def set_number_of_annotation_signals(handle: int, annot_signals: int) -> int:
"""
Set number of annotation signals.
Parameters:
- handle: int, file handle
- annot_signals: int, annotation signal count
Returns:
int: 0 on success, error code on failure
"""Low-level functions for configuring signal-specific header information.
def set_label(handle: int, edfsignal: int, label: Union[str, bytes]) -> int:
"""
Set signal label.
Parameters:
- handle: int, file handle
- edfsignal: int, signal number
- label: str or bytes, signal label
Returns:
int: 0 on success, error code on failure
"""
def set_physical_dimension(handle: int, edfsignal: int, phys_dim: Union[str, bytes]) -> int:
"""Set physical dimension (units) for signal."""
def set_physical_maximum(handle: int, edfsignal: int, phys_max: float) -> int:
"""Set physical maximum value for signal."""
def set_physical_minimum(handle: int, edfsignal: int, phys_min: float) -> int:
"""Set physical minimum value for signal."""
def set_digital_maximum(handle: int, edfsignal: int, dig_max: int) -> int:
"""Set digital maximum value for signal."""
def set_digital_minimum(handle: int, edfsignal: int, dig_min: int) -> int:
"""Set digital minimum value for signal."""
def set_samples_per_record(handle: int, edfsignal: int, smp_per_record: int) -> int:
"""
Set samples per record for signal.
Parameters:
- handle: int, file handle
- edfsignal: int, signal number
- smp_per_record: int, samples per data record
Returns:
int: 0 on success, error code on failure
"""
def set_transducer(handle: int, edfsignal: int, transducer: Union[str, bytes]) -> int:
"""Set transducer description for signal."""
def set_prefilter(handle: int, edfsignal: int, prefilter: Union[str, bytes]) -> int:
"""Set prefilter description for signal."""Low-level functions for positioning within signal data streams.
def tell(handle: int, edfsignal: int) -> int:
"""
Get current read position in signal.
Parameters:
- handle: int, file handle
- edfsignal: int, signal number
Returns:
int: Current sample position
"""
def seek(handle: int, edfsignal: int, offset: int, whence: int) -> int:
"""
Seek to position in signal.
Parameters:
- handle: int, file handle
- edfsignal: int, signal number
- offset: int, offset in samples
- whence: int, seek mode (0=absolute, 1=relative, 2=from end)
Returns:
int: New position or error code
"""
def rewind(handle: int, edfsignal: int):
"""
Rewind signal to beginning.
Parameters:
- handle: int, file handle
- edfsignal: int, signal number
"""Low-level functions for writing annotations with different text encodings.
def write_annotation_latin1(handle: int, onset: int, duration: int,
description: Union[str, bytes]) -> int:
"""
Write annotation with Latin-1 encoding.
Parameters:
- handle: int, file handle
- onset: int, onset time in 100ns units
- duration: int, duration in 100ns units
- description: str or bytes, annotation text
Returns:
int: 0 on success, error code on failure
"""
def write_annotation_utf8(handle: int, onset: int, duration: int,
description: Union[str, bytes]) -> int:
"""
Write annotation with UTF-8 encoding.
Parameters:
- handle: int, file handle
- onset: int, onset time in 100ns units
- duration: int, duration in 100ns units
- description: str or bytes, annotation text
Returns:
int: 0 on success, error code on failure
"""Low-level annotation data structure.
class EdfAnnotation:
"""
EDF annotation data structure.
Attributes:
- onset: int, annotation onset time
- duration: int, annotation duration
- annotation: str, annotation text
"""
onset: int
duration: int
annotation: strError code dictionaries for interpreting function return values.
# Error code mappings
open_errors: Dict[int, str] # File opening error codes
write_errors: Dict[int, str] # Writing error codesimport pyedflib
import numpy as np
from datetime import datetime
# Generate sample data
n_channels = 2
n_samples = 1000
sample_rate = 256
data = np.random.randn(n_channels, n_samples) * 100 # microvolts
# Open file for writing
handle = pyedflib.open_file_writeonly('lowlevel.edf', pyedflib.FILETYPE_EDFPLUS, n_channels)
if handle < 0:
raise RuntimeError(f"Failed to open file: {pyedflib.open_errors.get(handle, 'Unknown error')}")
try:
# Set file header
pyedflib.set_patientname(handle, b"Test Patient")
pyedflib.set_patientcode(handle, b"TP001")
pyedflib.set_technician(handle, b"Dr. Test")
pyedflib.set_equipment(handle, b"Test System")
# Set start date/time
now = datetime.now()
pyedflib.set_startdatetime(handle, now.year, now.month, now.day,
now.hour, now.minute, now.second)
# Set data record duration (1 second)
pyedflib.set_datarecord_duration(handle, 1.0)
# Configure each signal
for ch in range(n_channels):
pyedflib.set_label(handle, ch, f"CH{ch+1}".encode())
pyedflib.set_physical_dimension(handle, ch, b"uV")
pyedflib.set_physical_maximum(handle, ch, 500.0)
pyedflib.set_physical_minimum(handle, ch, -500.0)
pyedflib.set_digital_maximum(handle, ch, 32767)
pyedflib.set_digital_minimum(handle, ch, -32768)
pyedflib.set_samples_per_record(handle, ch, sample_rate)
pyedflib.set_transducer(handle, ch, b"Electrode")
pyedflib.set_prefilter(handle, ch, b"HP:0.1Hz")
# Write data using block write
data_interleaved = data.T.flatten() # Interleave channels
samples_written = pyedflib.blockwrite_physical_samples(handle, data_interleaved)
print(f"Wrote {samples_written} samples")
# Add annotations
onset_100ns = int(2.5 * 1e7) # 2.5 seconds in 100ns units
duration_100ns = 0 # Point annotation
pyedflib.write_annotation_utf8(handle, onset_100ns, duration_100ns, b"Stimulus")
finally:
# Always close the file
result = pyedflib.close_file(handle)
if result != 0:
print(f"Warning: Error closing file: {pyedflib.write_errors.get(result, 'Unknown error')}")
print("Low-level EDF file created successfully!")Install with Tessl CLI
npx tessl i tessl/pypi-pyedflib