NWB alignment#

The NWB alignment file (<project>/.ethograph/alignment.nwb) stores trial timing, media file paths, and stream offsets. It is the single source of truth for “what file corresponds to what trial, and when did it start”.

See Pairing and alignment for the user-facing walkthrough. At runtime the same interface is available via dt.nwb_alignment on a loaded TrialTree.


Pairing media files#

class ethograph.io.pairing.SourceSpec(stream, folder=None, files=(), pattern=None, device=None, software=None, extension=None)[source]#

One media source of a rig: where its files are and how they map to trials/devices.

ethograph.io.pairing.discover_media(sources)[source]#

The pairing table: trial + one {stream}_{device} column per source/device, full paths.

Each cell is the file’s full path, so the table says where every file is and pair_media() needs no root; the NWB trials table receives the basenames.

  • pattern None: files natsorted; row i is trial i+1; device from device or the default.

  • pattern set: named groups build trial + device columns; a file the pattern does not match is skipped (logged at INFO).

  • Extensions: only those in ethograph.io.validation VIDEO_/AUDIO_/POSE_EXTENSIONS for the source’s stream; extension narrows a scanned folder to one of them (a DLC folder holds .h5 and .csv twins of every file).

  • Fail fast: two pattern-less sources with different file counts -> ValueError naming both counts; a pattern that matches no file at all -> ValueError; two files on one (trial, device) -> ValueError naming them; an empty folder -> ValueError.

  • A trial present in one source but not another leaves "" in the missing cell (like the current template).

Return type:

DataFrame

ethograph.io.pairing.pair_media(trial_table, stream_rates=None, session_wide=None, output_path=None, pose_fps=None, individuals=None, on_existing='extend')[source]#

Write .ethograph/alignment.nwb from a pairing table.

Each per-trial {stream}_{device} column becomes a trials-table column plus one ImageSeries whose segments start at the trial starts; session_wide streams each get one ImageSeries with a starting_time. start_time/stop_time are optional — omitted, they are inferred from the media files the table names.

What happens to a file that is already there is the caller’s decision, never the filesystem’s (on_existing). Annotating somebody’s NWB and building a session’s own alignment are different jobs, and a function that guessed between them by looking at the disk was eventually called from the path that wanted the other one.

on_existing="extend" (the default) adds to an existing NWB that already has a trials table — a neuroconv-written source — in place through edit_nwb(), never rebuilding it: per-trial columns from trial_table become new trial columns (row count must equal the file’s trials, else ValueError) and session_wide streams acquisition ImageSeries; existing acquisition names are left alone. on_existing="replace" deletes the file and writes it afresh — what a drop or the wizard does, because an alignment built from dropped files is derived data and rebuilding it must be idempotent.

Parameters:
  • trial_table (DataFrame) – trial + {stream}_{device} columns holding each file’s full path (as discover_media() returns); a bare filename is accepted when times are given. The NWB trials table receives the basenames. start_time/stop_time optional.

  • stream_rates (dict[str, float] | None) – Sampling rate per stream, e.g. {"video": 30.0, "audio": 48000.0}; a {stream}_{device} key ("video_cam-2": 60.0) overrides its stream’s rate.

  • session_wide (dict[str, tuple[str, float, float]] | None) – {"{stream}_{device}": (file, rate_hz, starting_time_s)} for streams that are one file spanning the whole session rather than one file per trial.

  • output_path (str | Path | None) – Where to write the .nwb file.

  • on_existing (str) – What an existing output_path means: "extend" pairs into its trials table, "replace" rebuilds it from scratch. Anything building a session’s own alignment passes "replace", so running it twice is the same as once.

  • pose_fps (float | None) – Frame rate for probing pose files, when inferring times from a table whose only media columns are pose_*.

  • individuals (Optional[Sequence[str]]) – The individuals this session labels. Recorded in the session record, which is their one home; a dataset’s individual dim is checked against it on load.

Return type:

The in-memory NWBFile.


Creating alignment files#

ethograph.io.nwb_alignment.make_nwb_alignment(nwb_path=None)[source]#

Create a EmpytAlignment from an NWB path, falling back to base EmpytAlignment.

ethograph.io.nwb_alignment.discover_nwb(source)[source]#

Find the session’s alignment record from the session folder or any file in it.

Search order: 1. <session>/.ethograph/alignment.nwb 2. Any .nwb file in <session>/.ethograph/

Return type:

Path | None

ethograph.io.nwb_alignment.sync_acquisition_for_streams(nwbfile, stream_rates, media_paths=None)[source]#

Create ImageSeries acquisition items for ALL external media streams.

Reads the trials table to discover {stream}_{device} columns. For each stream+device pair, creates an ImageSeries in nwbfile.acquisition with external_file, starting_frame, and rate (or timestamps if offsets are present).

Parameters:
  • nwbfile (NWBFile) – NWB file with a populated trials table.

  • stream_rates (dict[str, float]) – Mapping of stream name to sampling rate, e.g. {"video": 30.0, "audio": 44100.0, "pose": 30.0}. A {stream}_{device} key ("video_cam-2": 60.0) overrides its stream’s rate.

  • media_paths (DataFrame | None) – The pairing table the trials came from, one row per trial in the same order, whose media cells hold full paths. When given, external_file records those paths, so a reader finds the media without being told the folder; the trials table keeps the basenames either way.

Return type:

None

ethograph.io.nwb_alignment.edit_nwb(path)[source]#

Reading alignment metadata#

class ethograph.io.nwb_alignment.NWBAlignment(nwb_path)[source]#

Session metadata backed by an NWB file.

All external media (video, audio, pose) are stored as ImageSeries in acquisition with {stream}_{device} naming. Timing comes from rate or timestamps on the ImageSeries.

devices(stream)[source]#

Discover devices from trials table columns AND acquisition items.

Three sources, checked in order:

  1. Trials table columns (video_cam_1 -> device cam_1).

  2. Acquisition ImageSeries following {stream}_{device} naming.

  3. Acquisition ImageSeries whose external_file extensions match known media types (e.g. .mp4 -> video, .wav -> audio).

Return type:

list[str]

electrical_series()[source]#

Discover ElectricalSeries in acquisition. Returns list of {name, path, n_channels, rate}.

Return type:

list[dict]

get_media(trial, stream, device=None)[source]#

Return the trial table’s media filename for stream/device.

A stored value that is an absolute local path (e.g. baked in by a template authored on a different machine) is reduced to its basename, so callers can always safely join it onto their own video/audio/pose folder instead of resolving to a path that only existed on the authoring machine.

Return type:

str | None

get_stream_rate(stream, device=None)[source]#

Read the sampling rate for a stream from its acquisition ImageSeries.

Return type:

float | None

property has_real_timing: bool#

Whether the trials table has meaningful start/stop times.

Returns False when all trials have identical placeholder timing (e.g. start=0.0, stop=1.0 for every row), which indicates the NWB was generated without real session timing.

resolve_media_path(trial, stream, device=None, fallback_folder=None)[source]#

Resolve the full path for a media file.

  1. Try the ImageSeries external_file path for this trial (if on disk).

  2. Fallback: trial table filename + fallback_folder.

  3. Returns None if unresolvable.

A video may be an image folder (io/image_sequence.py), so “on disk” means media_exists(), never a bare isfile.

Return type:

str | None

stream_offset_for_trial(trial, stream, device=None)[source]#

Trial-relative time of sample 0 of the file that holds trial.

0.0 for per-trial files, negative for a session-wide file that started before the trial. The file is found by time (the trial’s start falls in its span), never by position – see _file_for_trial.

Return type:

float

class ethograph.io.nwb_alignment.TableAlignment(trials_df)[source]#

Alignment backed by a tabular dataframe with trial timing columns.

Expected columns are trial, start_time, and stop_time. This is used as a fallback when no suitable alignment NWB is available.

class ethograph.io.nwb_alignment.EmpytAlignment[source]#

Base session metadata interface with null-object defaults.

NWBAlignment overrides these with real NWB-backed implementations. When no NWB file is available, the base class is used directly.


NWB import helpers#

ethograph.io.nwb_import.read_trials_table(nwb)[source]#
Return type:

DataFrame

ethograph.io.nwb_import.probe_electrical_series(nwb)[source]#

List all ElectricalSeries in nwb.acquisition.

Return type:

list[dict]

ethograph.io.nwb_import.probe_label_sources(nwb)[source]#

List all potential interval label sources in the NWB file.

Return type:

list[dict]