Skip to content

Migrate annotation writing from deprecated AnnotationSeries to EventsTable #14

Description

@cboulay

Summary

NWBSinkConsumer writes annotation rows into a pynwb.misc.AnnotationSeries per table_name. As of pynwb 4.0.0 / NWB schema 2.10.0 (NWBEP001), AnnotationSeries is deprecated in favour of EventsTable in the new top-level /events group. Constructing an AnnotationSeries under pynwb >= 4.0 emits a UserWarning; reading existing files still works. We should migrate the writer.

From the pynwb docs:

DEPRECATED. Use an EventsTable instead, placed in the top-level /events group of the NWBFile.
timestampstimestamp column, dataannotation column, and source_description should record the origin of the events.

Current state

  • src/ezmsg/nwb/writer.py
    • _prep_annotation_series() (~L792) — creates an empty pynwb.misc.AnnotationSeries with resizable H5DataIO data/timestamps datasets, nwbfile.add_acquisition(series), then flush + reopen to grab the live h5py datasets.
    • write_annotation() (~L834) — resizes and appends one (timestamp, data) pair directly on the h5py datasets.
    • NWBSink.on_annotation() (~L999) — duck-typed INPUT_ANNOTATION subscriber, routes flatten_for_table()["data"] to write_annotation.
    • The reopen path (~L760) re-resolves annotation_data / annotation_ts after each flush.
  • src/ezmsg/nwb/pipeline_settings.py — the typed-column sink is described as an alternative to "the JSON-AnnotationSeries path"; docstrings reference AnnotationSeries and need updating.

Proposed change

  • Replace the per-table_name AnnotationSeries in /acquisition with a pynwb.event.EventsTable per table_name added via nwbfile.add_events_table(...) (lands in /events).
  • Columns: built-in timestamp (TimestampVectorData) plus an annotation text column carrying today's data string. Set source_description to something meaningful (e.g. the producer/table origin) instead of the current generic description.
  • Append with table.add_event(timestamp=..., annotation=...) on the live re-opened table, mirroring the existing TimeIntervals pattern in pipeline_settings.py (table.add_interval(...) after _flush_io(reopen=True)) rather than hand-managing resizable h5py datasets. That should let us drop annotation_data / annotation_ts from NWBSinkState and the reopen re-resolution logic.
  • Keep the existing timestamp convention (timestamp - start_timestamp once a data message has anchored the file; wall-clock passthrough before that) — the docstring on write_annotation still applies.

Open questions

  • Dependency floor. pyproject.toml pins only pynwb (unversioned); the current lockfile resolves to 3.1.3, which has no pynwb.event module. Migrating requires pynwb>=4.0.0, which also raises the minimum NWB schema to 2.10.0. Do we bump unconditionally, or keep an AnnotationSeries fallback for older pynwb? Recommendation: bump the floor — dual-path writing doubles the test surface for a sink that is not yet widely deployed.
  • Backwards compatibility for readers. Files written previously carry /acquisition/<name> AnnotationSeries; new files carry /events/<name>. Nothing in reader.py / iterator.py / slicer.py currently reads annotations back, so this is only a concern for external consumers — worth a note in the changelog/README.
  • Per-row flush cost. add_event on a re-opened DynamicTable may be more expensive per row than the current single-element h5py resize. Worth a quick benchmark if annotation rates are high.
  • API surface. Should write_annotation gain a way to pass extra typed columns (the EventsTable supports arbitrary add_column), or stay string-only to keep pipeline_settings.py as the typed path?

Acceptance criteria

  • Annotations land in /events/<table_name> as an EventsTable, one row per message.
  • No UserWarning about deprecated types when writing with pynwb >= 4.0.
  • Multiple table_names still produce distinct tables (tests/test_writer.py::... around L651).
  • Existing annotation tests updated: tests/test_writer.py (~L580, ~L651), tests/test_integration.py (~L436).
  • Docstrings in writer.py and pipeline_settings.py no longer reference AnnotationSeries as the current mechanism.
  • pyproject.toml dependency floor updated and uv.lock refreshed.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions