You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Neither NWBSlicer nor NWBAxisArrayIterator knows about EventsTable. Once #14 lands and annotations are written to the top-level /events group, they will be invisible on readback. We should discover EventsTable instances as event streams, and — for files written before the migration — keep discovering AnnotationSeries too.
Why EventsTable won't be found today
NWBSlicer._load() (src/ezmsg/nwb/slicer.py:200) discovers exactly two kinds of thing:
TimeSeries — _extract_timeseries_from_container() over the root and each processing module (slicer.py:261-263).
An EventsTable lives in nwbfile.events, not nwbfile.intervals, and is not a TimeSeries — so it falls through both. Even if it were routed into the interval branch it would break immediately: that code reads table.start_time / table.stop_time, and an EventsTable has a single timestamp column instead.
AnnotationSeries, by contrast, is a TimeSeries subclass, so it is currently picked up by the timeseries branch — with rate inferred from np.diff(timestamps) variance (slicer.py:300-304), which for sparse annotations lands on rate=0.0 and hence a CoordinateAxis. That's roughly the right behaviour, but it's incidental rather than intentional and it's untested through the slicer/iterator (the existing tests at tests/test_integration.py:436 and tests/test_writer.py:580,651 assert on the raw pynwb container, never on iterated messages).
Proposed change
Discovery (NWBSlicer._load)
Add a third discovery pass over getattr(nwbfile, "events", None), emitting StreamInfo(is_event=True, ...) per EventsTable, honouring the stream_keys filter the same way the interval pass does.
Columns: use the timestamp column for the time vector; project the remaining columns (e.g. annotation, plus any user-added columns) into the chCoordinateAxis and a stringified dset, mirroring slicer.py:238-239.
stop_time: interval tables use table.stop_time[-1]; an EventsTable has no duration by default (there is an optional DurationVectorData). Use timestamp[-1] (plus duration[-1] when a duration column is present).
Keep the existing AnnotationSeries path working. Preferably make it explicit — recognize pynwb.misc.AnnotationSeries in the timeseries branch and force rate=0.0 / event-like handling rather than relying on the variance heuristic to land there by accident.
Abstract the event time accessor
table.start_time is hardcoded in three places on the event path and needs to become column-agnostic (start_time for TimeIntervals, timestamp for EventsTable):
slicer.py:236-237 (start/stop time bounds)
slicer.py:472 (read_by_time)
iterator.py:110 (_build_chunk_messages_static)
Simplest fix: store the resolved timestamp array on StreamInfo.timestamps (already done) and use that in the two read paths instead of re-reaching into table_ref. That would let table_ref become purely informational and remove the coupling entirely.
Iterator
_preload() (iterator.py:305-310) and _build_chunk_messages_static() (iterator.py:107-127) already branch on info.is_event generically, so once discovery produces well-formed event StreamInfos they should need little or no change beyond the table.start_time fix above. Worth confirming the one-message-per-event emission is the behaviour we want for annotations, or whether all events in a chunk should coalesce into a single message.
Backwards compatibility
Files written by the current writer keep working: AnnotationSeries in /acquisition is still discovered.
A file could contain both (e.g. written across a version bump, or an AnnotationSeries from another tool alongside our EventsTable). Both should surface as independent streams. Decide what happens on a stream-key collision — probably let the NWB container names disambiguate, since they can't collide within one file across different groups... except that our stream dict is keyed by bare name. Worth an explicit tie-break rule.
Summary
Neither
NWBSlicernorNWBAxisArrayIteratorknows aboutEventsTable. Once #14 lands and annotations are written to the top-level/eventsgroup, they will be invisible on readback. We should discoverEventsTableinstances as event streams, and — for files written before the migration — keep discoveringAnnotationSeriestoo.Why
EventsTablewon't be found todayNWBSlicer._load()(src/ezmsg/nwb/slicer.py:200) discovers exactly two kinds of thing:getattr(nwbfile, "intervals", None), iterated atslicer.py:232-258, producingStreamInfo(is_event=True, table_ref=table)._extract_timeseries_from_container()over the root and each processing module (slicer.py:261-263).An
EventsTablelives innwbfile.events, notnwbfile.intervals, and is not aTimeSeries— so it falls through both. Even if it were routed into the interval branch it would break immediately: that code readstable.start_time/table.stop_time, and anEventsTablehas a singletimestampcolumn instead.AnnotationSeries, by contrast, is aTimeSeriessubclass, so it is currently picked up by the timeseries branch — withrateinferred fromnp.diff(timestamps)variance (slicer.py:300-304), which for sparse annotations lands onrate=0.0and hence aCoordinateAxis. That's roughly the right behaviour, but it's incidental rather than intentional and it's untested through the slicer/iterator (the existing tests attests/test_integration.py:436andtests/test_writer.py:580,651assert on the raw pynwb container, never on iterated messages).Proposed change
Discovery (
NWBSlicer._load)getattr(nwbfile, "events", None), emittingStreamInfo(is_event=True, ...)perEventsTable, honouring thestream_keysfilter the same way the interval pass does.timestampcolumn for the time vector; project the remaining columns (e.g.annotation, plus any user-added columns) into thechCoordinateAxisand a stringifieddset, mirroringslicer.py:238-239.stop_time: interval tables usetable.stop_time[-1]; anEventsTablehas no duration by default (there is an optionalDurationVectorData). Usetimestamp[-1](plusduration[-1]when a duration column is present).AnnotationSeriespath working. Preferably make it explicit — recognizepynwb.misc.AnnotationSeriesin the timeseries branch and forcerate=0.0/ event-like handling rather than relying on the variance heuristic to land there by accident.Abstract the event time accessor
table.start_timeis hardcoded in three places on the event path and needs to become column-agnostic (start_timeforTimeIntervals,timestampforEventsTable):slicer.py:236-237(start/stop time bounds)slicer.py:472(read_by_time)iterator.py:110(_build_chunk_messages_static)Simplest fix: store the resolved timestamp array on
StreamInfo.timestamps(already done) and use that in the two read paths instead of re-reaching intotable_ref. That would lettable_refbecome purely informational and remove the coupling entirely.Iterator
_preload()(iterator.py:305-310) and_build_chunk_messages_static()(iterator.py:107-127) already branch oninfo.is_eventgenerically, so once discovery produces well-formed eventStreamInfos they should need little or no change beyond thetable.start_timefix above. Worth confirming the one-message-per-event emission is the behaviour we want for annotations, or whether all events in a chunk should coalesce into a single message.Backwards compatibility
AnnotationSeriesin/acquisitionis still discovered.EventsTablein/events.AnnotationSeriesfrom another tool alongside ourEventsTable). Both should surface as independent streams. Decide what happens on a stream-key collision — probably let the NWB container names disambiguate, since they can't collide within one file across different groups... except that our stream dict is keyed by bare name. Worth an explicit tie-break rule.pynwb.eventonly exists on pynwb >= 4.0.0, so the new discovery pass must be import-guarded (or gated on the dependency floor decided in Migrate annotation writing from deprecated AnnotationSeries to EventsTable #14) if we support older pynwb on the read side.Acceptance criteria
EventsTablein/eventsis discovered byNWBSlicerand appears instream_names.NWBSlicer.read_by_time()returns its events with correct absolute timestamps (ts_offapplied).NWBAxisArrayIteratoremits those events in the correct chunks, interleaved with continuous streams.AnnotationSeriesin/acquisitionis still discovered and read back — with a test that goes through the slicer/iterator, not just raw pynwb.stream_keysfiltering applies toEventsTablestreams.EventsTableextra columns (beyondtimestamp/annotation) surface on thechaxis.References
pynwb.event.EventsTable