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.
EDF files that give each channel its own offset need one more decision, described in
Handling Channels with Heterogeneous Offsets.
A file that samples some of its signals at a different rate than the rest holds them in separate
streams and is read one stream at a time: list them with
EDFRecordingInterface.get_stream_names(file_path=file_path) and pass the one you want as
stream_name.
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)
# Add subject information (required for DANDI upload)
metadata["Subject"] = dict(subject_id="subject1", species="Mus musculus", sex="M", age="P30D")
# 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)
# Add subject information (required for DANDI upload)
metadata["Subject"] = dict(subject_id="subject1", species="Mus musculus", sex="M", age="P30D")
# 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_non_electrical_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,
)
bpm_channels_metadata_key = "time_series_pulse_rate" # Beats per minute unit
bpm_interface = EDFAnalogInterface(
file_path=file_path,
channels_to_include=["PR"], # Beats per minute units
metadata_key=bpm_channels_metadata_key,
)
microvolt_channels_metadata_key = "time_series_plethysmography" # Microvolt unit
microvolt_interface = EDFAnalogInterface(
file_path=file_path,
channels_to_include=["Pleth"], # Microvolt units
metadata_key=microvolt_channels_metadata_key,
)
# Combine all interfaces
converter = ConverterPipe(
data_interfaces=[
recording_interface,
trigger_interface,
percent_interface,
bpm_interface,
microvolt_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)
# Add subject information (required for DANDI upload)
metadata["Subject"] = dict(subject_id="subject1", species="Mus musculus", sex="M", age="P30D")
# 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"
},
bpm_channels_metadata_key: {
"name": "TimeSeriesPulseRate",
"description": "Pulse rate reported by the patient monitor"
},
microvolt_channels_metadata_key: {
"name": "TimeSeriesPlethysmography",
"description": "Plethysmography waveform from the pulse oximeter"
}
}
}
# 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)