European Data Format (EDF) conversion#

Install NeuroConv with the additional dependencies necessary for reading EDF data.

pip install "neuroconv[edf]"

Converting EDF Electrode Channels#

The EDFRecordingInterface is designed specifically for electrode recording channels that will be stored as an ElectricalSeries in the NWB file. Other auxiliary signal channels should be excluded using the channels_to_skip parameter.

from datetime import datetime
from zoneinfo import ZoneInfo
from pathlib import Path
from neuroconv.datainterfaces import EDFRecordingInterface

file_path = f"{ECEPHY_DATA_PATH}/edf/electrode_and_analog_data/electrode_and_analog_data.edf"

# Get all channel IDs to identify which ones to skip
all_channels = EDFRecordingInterface.get_available_channel_ids(file_path)
print(f"Available channels: {all_channels}")

# Identify non-electrode channels that should be skipped
# We don't have an automatic way to detect non-electrode channels, user passes this knowledge here
channels_to_skip = ["TRIG", "OSAT", "PR", "Pleth"]  # Example: trigger and physiological monitoring

# Create interface with channels to skip
interface = EDFRecordingInterface(
    file_path=file_path,
    channels_to_skip=channels_to_skip
)

# Extract what metadata we can from the source files
metadata = interface.get_metadata()
# For data provenance we add the time zone information to the conversion
session_start_time = metadata["NWBFile"]["session_start_time"].replace(tzinfo=ZoneInfo("US/Pacific"))
metadata["NWBFile"].update(session_start_time=session_start_time)

# Choose a path for saving the nwb file and run the conversion
nwbfile_path = f"{path_to_save_nwbfile}"
interface.run_conversion(nwbfile_path=nwbfile_path, metadata=metadata, overwrite=True)

Converting Auxiliary EDF Channels as TimeSeries#

Auxiliary signals such as physiological monitoring signals, triggers, or auxiliary data should be stored as generic TimeSeries objects rather than ElectricalSeries. Use the dedicated EDFAnalogInterface to handle these channels.

Important: TimeSeries objects in PyNWB require all channels to have the same physical unit. If your auxiliary channels have different units (e.g., triggers with no unit, OSAT in %, PR in bpm), you’ll need to create separate EDFAnalogInterface instances for each unit type:

from datetime import datetime
from zoneinfo import ZoneInfo
from pathlib import Path
from neuroconv.datainterfaces import EDFAnalogInterface

file_path = f"{ECEPHY_DATA_PATH}/edf/electrode_and_analog_data/electrode_and_analog_data.edf"

# Example: Trigger channels (no unit)
trigger_channels = ["TRIG"]  # Trigger signals

interface = EDFAnalogInterface(
    file_path=file_path,
    channels_to_include=trigger_channels
)

# Extract metadata and add timezone information
metadata = interface.get_metadata()
# For data provenance we add the time zone information to the conversion
session_start_time = metadata["NWBFile"]["session_start_time"].replace(tzinfo=ZoneInfo("US/Pacific"))
metadata["NWBFile"].update(session_start_time=session_start_time)

# Choose a path for saving the nwb file and run the conversion
nwbfile_path = f"{path_to_save_nwbfile}"
interface.run_conversion(nwbfile_path=nwbfile_path, metadata=metadata, overwrite=True)

Combining Electrode and Auxiliary Channels#

To convert both electrode and auxiliary channels into a single NWB file, use the ConverterPipe with multiple interfaces. Remember to group auxiliary channels by their unit types:

from datetime import datetime
from zoneinfo import ZoneInfo
from pathlib import Path
from neuroconv import ConverterPipe
from neuroconv.datainterfaces import EDFRecordingInterface, EDFAnalogInterface
from neuroconv.utils import dict_deep_update

file_path = f"{ECEPHY_DATA_PATH}/edf/electrode_and_analog_data/electrode_and_analog_data.edf"

# Define the channels to process
all_non_electrical_channels = ["TRIG", "OSAT", "PR", "Pleth"]  # All auxiliary channels

# Create electrode interface (skip all auxiliary channels)
recording_interface = EDFRecordingInterface(
    file_path=file_path,
    channels_to_skip=all_auxiliary_channels,

)

# Create separate analog interfaces for each unit type
trigger_channels_metadata_key = "time_series_trigger"  # No unit
trigger_interface = EDFAnalogInterface(
    file_path=file_path,
    channels_to_include=["TRIG"],  # No unit
    metadata_key=trigger_channels_metadata_key
)

percent_channels_metadata_key = "time_series_oxygen"  # Percentage unit
percent_interface = EDFAnalogInterface(
    file_path=file_path,
    channels_to_include=["OSAT"],  # Percentage units
    metadata_key=percent_channels_metadata_key,
)

# Combine all interfaces
converter = ConverterPipe(
    data_interfaces=[recording_interface, trigger_interface, percent_interface],

)

# Extract metadata and add timezone information
metadata = converter.get_metadata()
# For data provenance we add the time zone information to the conversion
session_start_time = metadata["NWBFile"]["session_start_time"].replace(tzinfo=ZoneInfo("US/Pacific"))
metadata["NWBFile"].update(session_start_time=session_start_time)

# REQUIRED: Customize TimeSeries names when using multiple analog interfaces
user_edited_metadata = {
    "TimeSeries": {
        trigger_channels_metadata_key: {
            "name": "TimeSeriesTrigger",
            "description": "Trigger signals from EDF file"
        },
        percent_channels_metadata_key: {
            "name": "TimeSeriesOxygen",
            "description": "Oxygen saturation monitoring data"
        }
    }
}

# The metadata_key parameter ensures each interface creates entries with the correct names
metadata = dict_deep_update(metadata, user_edited_metadata)

# Convert all channel types to a single NWB file
nwbfile_path = f"{path_to_save_nwbfile}"
converter.run_conversion(nwbfile_path=nwbfile_path, metadata=metadata, overwrite=True)