Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ env/
# Ruff
.ruff_cache/

# MkDocs
site/

# Context and planning files
.context/
CLAUDE.md
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The documentation including installation instructions, examples, and API referen
- OTB Systems (supported)
- EDF/BDF(+) (supported, including annotations)
- WFDB (supported, including annotations)
- XDF/Lab Streaming Layer (supported, multi-stream)
- Generic CSV (supported with auto-detection)
- Noraxon (planned)

Expand All @@ -28,6 +29,7 @@ The documentation including installation instructions, examples, and API referen
- Specialized format detection for CSV files
- Custom importers for system-specific formats
- Automatic annotation loading (WFDB, planned for EDF+/BDF+ and EEGLAB's .set files)
- LSL timestamp preservation for XDF files (for synchronization)

- Export to standardized formats:
- EDF/BDF(+) with channels.tsv metadata (automatically selects format based on signal properties, preserves annotations)
Expand Down
175 changes: 175 additions & 0 deletions docs/api/importers/xdf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# XDF Importer

The `XDFImporter` class handles importing data from XDF (Extensible Data Format) files, the native format for Lab Streaming Layer (LSL) recordings. It supports multi-stream files with different sampling rates and data types.

## Class Documentation

::: emgio.importers.xdf
options:
show_root_heading: true
show_source: true
members: true

## Usage Examples

### Basic Loading

```python
from emgio import EMG
from emgio.importers.xdf import XDFImporter

# Method 1: Using EMG.from_file (recommended)
emg = EMG.from_file('recording.xdf')

# Method 2: Using the importer directly
importer = XDFImporter()
emg = importer.load('recording.xdf')
```

### Exploring File Contents

Before loading, explore what streams are available:

```python
from emgio.importers.xdf import summarize_xdf

summary = summarize_xdf('recording.xdf')
print(summary)

# Output example:
# XDF File: recording.xdf
# ----------------------------------------
# Stream 1: MyEEG (EEG)
# Channels: 8, Rate: 256.0 Hz
# Samples: 15360, Duration: 60.0s
# Stream 2: MyEMG (EMG)
# Channels: 2, Rate: 2048.0 Hz
# Samples: 122880, Duration: 60.0s
# Stream 3: Markers (Markers)
# Channels: 1, Rate: 0.0 Hz (irregular)
# Samples: 10
```

### Selective Stream Loading

```python
# Load only specific stream types
emg = EMG.from_file('recording.xdf', stream_types=['EMG'])

# Load multiple types
emg = EMG.from_file('recording.xdf', stream_types=['EMG', 'EEG'])

# Load by stream name
emg = EMG.from_file('recording.xdf', stream_names=['MyEMGDevice'])

# Load by stream ID
emg = EMG.from_file('recording.xdf', stream_ids=[2])
```

### Setting Default Channel Type

```python
# For streams without explicit channel type metadata
emg = EMG.from_file('recording.xdf', default_channel_type='EMG')
```

### Preserving LSL Timestamps

```python
# Include original LSL timestamps as additional channels
emg = EMG.from_file('recording.xdf', include_timestamps=True)

# Each stream gets a "{stream_name}_LSL_timestamps" channel
# Useful for synchronization with other LSL-recorded data
```

## File Format Support

The XDF importer supports:

1. Single-stream and multi-stream XDF files
2. Compressed XDF files (.xdfz)
3. Numeric data types: float32, float64, int8, int16, int32, int64
4. Different sampling rates across streams (with resampling)
5. Channel labels from stream descriptors

## Stream Selection Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `stream_names` | `list[str]` | Filter by stream names (case-insensitive) |
| `stream_types` | `list[str]` | Filter by stream types (e.g., "EMG", "EEG") |
| `stream_ids` | `list[int]` | Filter by stream IDs |
| `default_channel_type` | `str` | Default type for channels without explicit type |
| `include_timestamps` | `bool` | If True, add LSL timestamp channels for each stream |

## Return Values

The `load()` method returns an `EMG` object with:

### Signals (pandas.DataFrame)
- Time-indexed signal data
- Channels as columns
- Resampled to common time base if multiple streams

### Channels (dict)
For each channel:
- `channel_type`: Inferred or default type
- `physical_dimension`: Unit (default "a.u.")
Comment thread
neuromechanist marked this conversation as resolved.
- `sample_frequency`: Effective sampling rate
- `stream_name`: Original stream name
- `stream_id`: Original stream ID

### Metadata (dict)
- `device`: "XDF"
- `source_file`: Path to the XDF file
- `stream_count`: Number of streams in file
- `stream_names`: List of all stream names
- `stream_types`: List of all stream types

## Helper Classes

### XDFSummary

Provides an overview of the XDF file:

```python
summary = summarize_xdf('recording.xdf')

# Access all streams
for stream in summary.streams:
print(f"{stream.name}: {stream.channel_count} channels")

# Find streams by type
emg_streams = summary.get_streams_by_type('EMG')

# Find stream by name
stream = summary.get_stream_by_name('MyDevice')
```

### XDFStreamInfo

Contains metadata for a single stream:

- `stream_id`: Unique stream identifier
- `name`: Stream name
- `stream_type`: Stream type (EEG, EMG, etc.)
- `channel_count`: Number of channels
- `nominal_srate`: Declared sampling rate
- `effective_srate`: Actual measured sampling rate
- `channel_format`: Data format (float32, string, etc.)
- `source_id`: Source identifier
- `hostname`: Recording machine hostname
- `sample_count`: Number of samples
- `duration_seconds`: Recording duration
- `channel_labels`: List of channel names

## Implementation Notes

1. **String/Marker Streams:** Streams with `channel_format='string'` are excluded from signal loading but appear in summaries.

2. **Time Alignment:** When loading multiple streams, timestamps are aligned to start at 0.

3. **Resampling:** Multiple streams with different rates are resampled using linear interpolation to the highest rate.
Comment thread
neuromechanist marked this conversation as resolved.

4. **Channel Naming:** Channels are prefixed with stream name to avoid conflicts (e.g., "StreamName_ChannelLabel").
173 changes: 173 additions & 0 deletions docs/examples/xdf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# XDF Example

This example demonstrates how to work with XDF files from Lab Streaming Layer (LSL) recordings, including exploring multi-stream files and selective loading.

## Exploring XDF Contents

XDF files often contain multiple streams. Before loading, explore what's available:

```python
from emgio.importers.xdf import summarize_xdf

# Summarize all streams in the file
summary = summarize_xdf('examples/multi_stream_test.xdf')
print(summary)
```

Output:
```
XDF File: examples/multi_stream_test.xdf
----------------------------------------
Stream 1: TestEEG (EEG)
Channels: 8, Rate: 256.0 Hz
Samples: 1280, Duration: 5.0s
Labels: EEG1, EEG2, EEG3, EEG4, EEG5, EEG6, EEG7, EEG8
Stream 2: TestEMG (EMG)
Channels: 2, Rate: 2048.0 Hz
Samples: 10240, Duration: 5.0s
Labels: EMG_L, EMG_R
Stream 3: TestMocap (Mocap)
Channels: 6, Rate: 120.0 Hz
Samples: 600, Duration: 5.0s
Labels: Marker1_X, Marker1_Y, Marker1_Z, Marker2_X, Marker2_Y, Marker2_Z
Stream 4: TestMarkers (Markers)
Channels: 1, Rate: 0.0 Hz (irregular)
Samples: 5
```

## Finding Specific Streams

```python
# Find all EMG streams
emg_streams = summary.get_streams_by_type('EMG')
for stream in emg_streams:
print(f"Found EMG stream: {stream.name} with {stream.channel_count} channels")

# Find a specific stream by name
mocap = summary.get_stream_by_name('TestMocap')
if mocap:
print(f"Mocap rate: {mocap.nominal_srate} Hz")
print(f"Mocap channels: {mocap.channel_labels}")
```

## Loading All Numeric Data

```python
from emgio import EMG

# Load all numeric streams (EEG, EMG, Mocap - excludes Markers)
emg = EMG.from_file('examples/multi_stream_test.xdf')

print(f"Total channels: {len(emg.channels)}")
print(f"Channel names: {list(emg.channels.keys())}")
```

## Selective Stream Loading

### Load by Stream Type

```python
# Load only EMG data
emg_data = EMG.from_file('examples/multi_stream_test.xdf', stream_types=['EMG'])
print(f"EMG channels: {list(emg_data.channels.keys())}")
# Output: ['EMG_L', 'EMG_R']

# Load EEG and EMG together
combined = EMG.from_file('examples/multi_stream_test.xdf', stream_types=['EEG', 'EMG'])
print(f"Combined channels: {len(combined.channels)}")
# Output: 10 (8 EEG + 2 EMG)
```

### Load by Stream Name

```python
# Load specific streams by name
emg_data = EMG.from_file('examples/multi_stream_test.xdf', stream_names=['TestEMG'])
```

## Working with Multi-Rate Data

When loading streams with different sampling rates, they're resampled to a common time base:

```python
# Load EEG (256 Hz) and EMG (2048 Hz)
combined = EMG.from_file('examples/multi_stream_test.xdf', stream_types=['EEG', 'EMG'])

# Check the resulting sample rate (will be the highest: 2048 Hz)
Comment thread
neuromechanist marked this conversation as resolved.
first_channel = list(combined.channels.keys())[0]
print(f"Sample rate: {combined.channels[first_channel]['sample_frequency']} Hz")
```

## Preserving LSL Timestamps

XDF files contain per-sample LSL timestamps. To preserve these for synchronization:

```python
# Load with timestamp channels
emg = EMG.from_file('examples/multi_stream_test.xdf',
stream_types=['EMG'],
include_timestamps=True)

# Each stream gets a timestamp channel
print(list(emg.channels.keys()))
# ['EMG_L', 'EMG_R', 'TestEMG_LSL_timestamps']

# Access the original LSL timestamps
ts = emg.signals['TestEMG_LSL_timestamps']
print(f"First timestamp: {ts.iloc[0]:.6f}s")
print(f"Last timestamp: {ts.iloc[-1]:.6f}s")
```

## Exporting to EDF

After loading, export to EDF/BDF format:

```python
# Load EMG streams with timestamps for synchronization
emg = EMG.from_file('examples/multi_stream_test.xdf',
stream_types=['EMG'],
include_timestamps=True)

# Export to EDF (timestamps are preserved as a channel)
emg.to_edf('output_emg.edf')

# Verify the export
emg_reloaded = EMG.from_file('output_emg.edf')
print(f"Exported channels: {list(emg_reloaded.channels.keys())}")
```

## Complete Workflow Example

```python
from emgio import EMG
from emgio.importers.xdf import summarize_xdf

# 1. Explore the file
summary = summarize_xdf('recording.xdf')
print(summary)

# 2. Identify streams of interest
emg_streams = summary.get_streams_by_type('EMG')
print(f"Found {len(emg_streams)} EMG streams")

# 3. Load selected data
emg = EMG.from_file('recording.xdf', stream_types=['EMG'])

# 4. Check loaded data
print(f"Channels: {list(emg.channels.keys())}")
print(f"Duration: {emg.signals.index[-1]:.1f}s")
print(f"Sample rate: {emg.channels[list(emg.channels.keys())[0]]['sample_frequency']} Hz")

# 5. Plot signals
emg.plot_signals(time_range=(0, 5))

# 6. Export
emg.to_edf('emg_export.edf', verify=True)
```

## Notes

- Marker streams (string data) are not loaded as signal channels
- When multiple streams are loaded, channels are prefixed with stream names
- Time indices are normalized to start at 0
- The `pyxdf` package is used internally for reading XDF files
Loading
Loading