Data Loading Tutorial#

This tutorial covers how to load EEG data from various formats into NeuRodent’s two main organizer classes:

  • LongRecordingOrganizer (LRO): loads and manages a single recording (one session, one animal).

  • AnimalOrganizer (AO): discovers and groups multiple recordings for one animal using file-path patterns, then creates LROs internally.

Most users will interact with AnimalOrganizer directly. Understanding the LRO helps when you need fine-grained control over how individual recordings are loaded.

Setup#

from pathlib import Path
import logging
from datetime import datetime

import numpy as np

from neurodent import LongRecordingOrganizer, AnimalOrganizer
from neurodent.loading import DiscoveredFile, FileDiscoverer

import spikeinterface.core as si

logging.basicConfig(
    format="%(asctime)s - %(levelname)s - %(message)s",
    level=logging.INFO,
)
/home/runner/work/neurodent/neurodent/.venv/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm

Part 1: LongRecordingOrganizer#

The first argument (item) of LongRecordingOrganizer accepts several types, depending on your data layout:

item type

Use case

str / Path (single file)

One recording in a standard format (EDF, Intan, NWB, …)

list[str] (multiple files)

Several files to concatenate into one long recording

DiscoveredFile (single-file)

One file returned by FileDiscoverer

DiscoveredFile (multi-file)

Paired files that together form one recording (e.g. .bin + .csv)

None

When passing a pre-loaded si.BaseRecording via the recording= parameter

The mode parameter selects the backend: "si" (SpikeInterface), "mne" (MNE-Python), or None (pre-created recording).

The optional extract_func tells LRO how to read the file(s). It can be a SpikeInterface extractor name (e.g. "read_edf"), a callable, or a file-path string (e.g. "readers.py:read_custom").

1. Loading a Standard Format (EDF)#

The simplest case: point LRO to a single file and specify a built-in SpikeInterface extractor.

Sample recordings#

sample_dataset() returns the recordings bundled with the package: a 60-second ColMajor .bin + Meta .csv pair per animal, and a 5-second .edf. Reading .edf needs pip install neurodent[readers].

from neurodent.data import sample_dataset

SAMPLE_DATA = sample_dataset()

from neurodent import set_channel_map

# The bundled recordings use Intan Port C/D names; map them to canonical labels
# so channel-based grouping works.
set_channel_map({
    "LMot": ["C-015", "D-015"], "RMot": ["C-016", "D-016"],
    "LBar": ["C-014", "D-014"], "RBar": ["C-017", "D-017"],
    "LHip": ["C-012", "D-012"], "RHip": ["C-019", "D-019"],
    "LAud": ["C-009", "D-009"], "RAud": ["C-022", "D-022"],
    "LVis": ["C-010", "D-010"], "RVis": ["C-021", "D-021"],
})
# Load an EDF file by passing a single path string
lro_edf = LongRecordingOrganizer(
    item=str(SAMPLE_DATA / "A10" / "A10_recording.edf"),
    mode="si",
    extract_func="read_edf",
    manual_datetimes=datetime(2023, 12, 13),
)

print(f"Sampling frequency: {lro_edf.meta.f_s} Hz")
print(f"Number of channels: {lro_edf.meta.n_channels}")
print(f"Duration: {lro_edf.LongRecording.get_total_duration():.1f} s")
Sampling frequency: 1000.0 Hz
Number of channels: 10
Duration: 5.0 s
2026-10-09 21:39:31,485 - INFO - Applying scale_to_uV to convert raw ADC data to microvolts
2026-10-09 21:39:31,486 - INFO - Recording already at target sampling rate (1000 Hz) or unable to determine, no resampling needed
2026-10-09 21:39:31,489 - INFO - Finalizing file timestamps
2026-10-09 21:39:31,489 - INFO - Using manual timestamps: 1 file end times specified
# Access the underlying SpikeInterface recording
recording = lro_edf.LongRecording

print(f"Recording type: {type(recording).__name__}")
print(f"Duration: {recording.get_total_duration():.1f} seconds")
Recording type: ScaleRecording
Duration: 5.0 seconds

2. Loading Multi-File Formats with DiscoveredFile#

Some formats pair a data file with a metadata sidecar (e.g. a .bin with a .csv). Wrap the paths in a DiscoveredFile so LRO treats them as a single recording.

A custom extract_func receives the DiscoveredFile and returns a si.BaseRecording. NeuRodent ships one for this format, so import it and pass it directly:

from neurodent.readers import read_bin_csv_pair

# NeuRodent ships this reader for the custom .bin and metadata .csv format.
# It reads the .bin as column-major (all samples of channel 0, then channel 1, ...)
# and takes the channel count and sampling rate from the csv.
# Two files that together form one recording
discovered = DiscoveredFile(
    paths=(
        str(SAMPLE_DATA / "A10" / "Cage 2 A10-0_ColMajor.bin"),
        str(SAMPLE_DATA / "A10" / "Cage 2 A10-0_Meta.csv"),
    ),
)

# Pass the inline function as extract_func
lro_bin = LongRecordingOrganizer(
    item=discovered,
    mode="si",
    extract_func=read_bin_csv_pair,
    manual_datetimes=datetime(2023, 12, 13),
)

print(f"Sampling frequency: {lro_bin.meta.f_s} Hz")
print(f"Number of channels: {lro_bin.meta.n_channels}")
print(f"Channel names: {lro_bin.meta.channel_names}")
Sampling frequency: 1000.0 Hz
Number of channels: 10
Channel names: ['C-009', 'C-010', 'C-012', 'C-014', 'C-015', 'C-016', 'C-017', 'C-019', 'C-021', 'C-022']
2026-10-09 21:39:31,509 - INFO - Recording already at target sampling rate (1000 Hz) or unable to determine, no resampling needed
2026-10-09 21:39:31,522 - INFO - Finalizing file timestamps
2026-10-09 21:39:31,523 - INFO - Using manual timestamps: 1 file end times specified

File-path string alternative#

Instead of passing the function object, you can name it as a string. NeuRodent ships this reader, so "neurodent.readers:read_bin_csv_pair" works from any installed copy. The "path/to/file.py:function_name" form loads a reader that is not installed, for example one kept alongside your analysis scripts:

# Same result, but the reader is loaded from a file
lro_bin_from_file = LongRecordingOrganizer(
    item=discovered,
    mode="si",
    extract_func="neurodent.readers:read_bin_csv_pair",
    manual_datetimes=datetime(2023, 12, 13),
)

print(f"Sampling frequency: {lro_bin_from_file.meta.f_s} Hz")
print(f"Number of channels: {lro_bin_from_file.meta.n_channels}")
Sampling frequency: 1000.0 Hz
Number of channels: 10
2026-10-09 21:39:31,528 - INFO - Recording already at target sampling rate (1000 Hz) or unable to determine, no resampling needed
2026-10-09 21:39:31,541 - INFO - Finalizing file timestamps
2026-10-09 21:39:31,541 - INFO - Using manual timestamps: 1 file end times specified

3. Other Standard Formats#

Any format supported by SpikeInterface can be loaded via mode="si" by passing the appropriate extractor name:

# Intan .rhd
lro = LongRecordingOrganizer(
    item="/path/to/recording.rhd",
    mode="si",
    extract_func="read_intan",
)

# NWB
lro = LongRecordingOrganizer(
    item="/path/to/file.nwb",
    mode="si",
    extract_func="read_nwb",
)

MNE-Python formats are available with mode="mne":

import mne

lro = LongRecordingOrganizer(
    item="/path/to/recording.fif",
    mode="mne",
    extract_func=mne.io.read_raw_fif,
    manual_datetimes=datetime(2023, 12, 13),
)

4. Concatenating Multiple Files#

Pass a list of paths to have LRO concatenate them in order:

lro_multi = LongRecordingOrganizer(
    item=["/path/to/session1.edf", "/path/to/session2.edf"],
    mode="si",
    extract_func="read_edf",
)

5. Pre-Loaded Recording Objects#

If you already have a SpikeInterface BaseRecording in memory (from any source, e.g. NumpyRecording, a loaded .nwb, custom processing, etc.), pass it directly to LRO with mode=None.

First, create the recording:

# Create a SpikeInterface recording from raw numpy data
num_channels = 8
sampling_frequency = 1000  # Hz
duration = 10  # seconds
num_samples = int(sampling_frequency * duration)

# Scaled to a realistic microvolt range
data = (50 * np.random.randn(num_samples, num_channels)).astype(np.float32)

recording_custom = si.NumpyRecording(
    traces_list=[data],
    sampling_frequency=sampling_frequency,
)

channel_ids = [f"CH{i:02d}" for i in range(num_channels)]
recording_custom = recording_custom.rename_channels(new_channel_ids=channel_ids)

print(f"Recording type: {type(recording_custom).__name__}")
print(f"Duration: {recording_custom.get_total_duration():.1f} s")
Recording type: ChannelSliceRecording
Duration: 10.0 s
# Pass any si.BaseRecording directly to LRO
lro_custom = LongRecordingOrganizer(
    item=None,
    mode=None,
    recording=recording_custom,
)

print(f"Sampling frequency: {lro_custom.meta.f_s}")
print(f"Number of channels: {lro_custom.meta.n_channels}")
print(f"Channel names: {lro_custom.meta.channel_names}")
Sampling frequency: 1000.0
Number of channels: 8
Channel names: ['CH00', 'CH01', 'CH02', 'CH03', 'CH04', 'CH05', 'CH06', 'CH07']
2026-10-09 21:39:31,553 - INFO - Recording already at target sampling rate (1000 Hz) or unable to determine, no resampling needed

6. Inspecting Loaded Data#

Every LRO exposes a meta attribute (RecordingMetadata) with key properties:

metadata = lro_edf.meta

print(f"Recording metadata: {metadata}")
print(f"Sampling frequency: {metadata.f_s} Hz")
print(f"Number of channels: {metadata.n_channels}")
print(f"Channel names: {metadata.channel_names}")
print(f"Units: {metadata.V_units}")
print(f"Duration: {lro_edf.file_durations} seconds")
Recording metadata: <neurodent.loading.recording_metadata.RecordingMetadata object at 0x7f658a0bf880>
Sampling frequency: 1000.0 Hz
Number of channels: 10
Channel names: ['C-009', 'C-010', 'C-012', 'C-014', 'C-015', 'C-016', 'C-017', 'C-019', 'C-021', 'C-022']
Units: µV
Duration: [5.0] seconds

Part 2: AnimalOrganizer#

In practice you rarely create LROs yourself. Instead, AnimalOrganizer discovers recordings for an animal automatically using patterns, which are format strings with placeholders that match parts of the file path.

Pattern Placeholders#

Placeholder

Meaning

{animal}

Animal identifier (e.g. A10, F22)

{session}

Session or day folder (e.g. day1, 2023-12-13)

{index}

File index within a session (when multiple files per session)

*

Standard glob wildcard matching any character

Example: "/data/{animal}/{session}/*.edf" matches files like /data/A10/day1/recording.edf and extracts animal="A10", session="day1".

Key Constructor Parameters#

AO internally uses FileDiscoverer to match files, groups them by session, and creates one LRO per session:

Parameter

Description

pattern

A single pattern string, or a list of patterns for multi-file formats

animal_id

Filter discoveries to one animal

skip_sessions

Glob patterns for sessions to exclude

truncate

Limit the number of sessions loaded

lro_kwargs

Dict of arguments forwarded to each LongRecordingOrganizer

7. Finding Recordings with FileDiscoverer#

FileDiscoverer scans the filesystem using placeholder patterns and returns DiscoveredFile objects. For multi-file formats, pass a list of patterns; files that share the same placeholder values are grouped automatically.

# Discover all bin/csv pairs in the bundled sample dataset
discoverer = FileDiscoverer([
    str(SAMPLE_DATA / "{animal}" / "*_ColMajor.bin"),
    str(SAMPLE_DATA / "{animal}" / "*_Meta.csv"),
])
discovered_files = discoverer.discover()

for f in discovered_files:
    print(f"Animal {f.metadata['animal']}: {[Path(p).name for p in f.paths]}")
Animal A10: ['Cage 2 A10-0_ColMajor.bin', 'Cage 2 A10-0_Meta.csv']
Animal F22: ['Cage 3 F22-0_ColMajor.bin', 'Cage 3 F22-0_Meta.csv']

8. Passing Patterns to AnimalOrganizer#

The same pattern syntax goes straight into AnimalOrganizer. The AO runs FileDiscoverer internally, groups the results by session, and builds LROs.

For example, the patterns below discover the Cage 2 A10-0_ColMajor.bin and Cage 2 A10-0_Meta.csv files in the bundled sample dataset under A10/ and group them into a single-session LRO:

# Multi-file pattern for paired bin/csv data
ao = AnimalOrganizer(
    pattern=[
        str(SAMPLE_DATA / "{animal}" / "*_ColMajor.bin"),
        str(SAMPLE_DATA / "{animal}" / "*_Meta.csv"),
    ],
    animal_id="A10",
    lro_kwargs={
        "mode": "si",
        "extract_func": "neurodent.readers:read_bin_csv_pair",
        "manual_datetimes": datetime(2023, 12, 13),
    },
)

print(f"Animal Organizer created for {ao.animal_id}")
Animal Organizer created for A10
2026-10-09 21:39:31,569 - INFO - Processing manual_datetimes configuration
2026-10-09 21:39:31,569 - INFO - Processing global manual datetimes starting at 2023-12-13 00:00:00
2026-10-09 21:39:31,570 - INFO - Computing continuous timeline for 1 animaldays (1 total items) starting at 2023-12-13 00:00:00
2026-10-09 21:39:31,570 - INFO - Ordered items for timeline: ['Cage 2 A10-0_ColMajor.bin...']
2026-10-09 21:39:31,571 - INFO - Recording already at target sampling rate (1000 Hz) or unable to determine, no resampling needed
2026-10-09 21:39:31,583 - INFO - Finalizing file timestamps
2026-10-09 21:39:31,584 - INFO - Using manual timestamps: 1 file end times specified
2026-10-09 21:39:31,584 - INFO - Item Cage 2 A10-0_ColMajor.bin...: duration = 60.0s (loaded with manual timestamp)
2026-10-09 21:39:31,584 - INFO - Timeline computed: 1 items, total duration 60.0s
2026-10-09 21:39:31,586 - INFO - Recording already at target sampling rate (1000 Hz) or unable to determine, no resampling needed
2026-10-09 21:39:31,597 - INFO - Finalizing file timestamps
2026-10-09 21:39:31,598 - INFO - Using manual timestamps: 1 file end times specified
2026-10-09 21:39:31,598 - INFO - AnimalOrganizer Timeline Summary:
LRO 0: 2023-12-13 00:00:00 -> 2023-12-13 00:01:00 (duration: 60.0s, items: 1, item: Cage 2 A10-0_ColMajor.bin...)

For single-file formats the pattern is just a string:

ao = AnimalOrganizer(
    pattern="/data/{animal}/{session}/*.edf",
    animal_id="A10",
    lro_kwargs={"mode": "si", "extract_func": "read_edf"},
)

Next Steps#