ezmsg.nwb.writer#
This module provides a sink node that writes incoming data to an NWB file.
Iteratively writing data to an NWB file has a few quirks.
- The main approach to iterative writing in the docs – wrapping the data source in a DataChunkIterator – does not work
well for our scenario. First, we would need to use some sort of queues to communicate between the data-receiving function and the thread / task that is yielding data to the iterator; this seems doable. Second, it’s not clear if each DataChunkIterator must exhaust itself before moving onto the next one or if they can run in parallel; this is a deal-breaker for us. Third, it seems impossible to add new TimeSeries after the io.write(nwbfile) process has begun; this is also a deal-breaker for us.
Instead, we use the “user-defined dataset write” approach, described here. With this approach, we use pynwb to construct the NWB structure, but we specify the data containers (both data and timestamps) as H5DataIO objects. After the NWB structure is written to disk, the containers expose h5py Dataset objects that we can resize and append to as data arrive.
This approach has some quirks that we have worked around and landed on the following strategies:
A - When all incoming streams are known a priori
Load metadata (shape, rate, channel labels) from a yaml or json file.
Create all H5DataIO objects for epochs and trials: id, start_time, stop_time, and custom “label” column. * Append dummy event to table. Remove this step after hdmf-dev/hdmf#1000
Create device, electrode group, and add all known electrodes.
Create all H5DataIO objects for continuous data (.data and .timestamps)
Attach .data and .timestamps to new TimeSeries, or new ElectricalSeries if electrodes are known.
Flush the NWBFile to disk – Slow!! but necessary to create h5py Datasets
Reopen the NWBFile in append mode – Fast; necessary to make tables appendable, but freezes the electrodes table.
Add any .rate attributes to the timestamps h5py datasets.
Get references to the .data and .timestamps datasets for each continuous data appending.
B - When streams are not known a priori
For the first message of a stream… * If it is continuous data, even size-zero
Create the H5DataIO objects for .data and .timestamps
- If it is the FIRST continuous data stream with channel labels then create the device, elec group and electrodes
“FIRST” limitation should go away: hdmf-dev/hdmf#1181
Attach .data and .timestamps to TimeSeries, or ElectricalSeries if electrodes are known (i.e. FIRST).
Flush the NWBFile to disk – Slow!! but necessary to create h5py Datasets
Add any .rate attributes to the timestamps h5py datasets
If it is an event… * Create all H5DataIO objects for epochs and trials: id, start_time, stop_time, and custom “label” column. * Append dummy event to table. Remove this step after hdmf-dev/hdmf#1000 * Flush the NWBFile to disk – Slow!! but necessary to create h5py Datasets * Reopen the NWBFile in append mode – Fast; necessary to make tables appendable, but freezes the electrodes table.
In this case, we must call io.write(nwbfile) once for each stream, which can significantly slow down performance for 1.5-4 seconds * number of unique streams. Thus, we strongly recommend running NWBSink in a separate process where possible.
C - When closing a file on quit or when splitting
When done appending to datasets and events tables, Flush the NWB file once more – Slow!!
If the file has no data other than the dummy event and recording is disabled, delete the file.
D - When opening the next file when splitting
Copy as much information from the old NWBFile to the new NWBFile as possible.
Close as in C.
Follow pattern A where the metadata comes from the old datasets instead of loaded from a file.
Classes
- class NWBSink(*args, settings=None, **kwargs)[source]#
Bases:
BaseConsumerUnit[NWBSinkSettings,AxisArray,NWBSinkConsumer]- Parameters:
settings (Settings | None)
- SETTINGS#
alias of
NWBSinkSettings
- INPUT_ANNOTATION = InputStream:unlocated[object]()#
- async on_annotation(msg)[source]#
Append one row to the per-table
AnnotationSeriesnamed bymsg.table_name.Ignores messages that don’t quack like an
NWBPointRowso other traffic on this stream (if any) doesn’t crash the sink. Any exception from the writer is logged and swallowed — annotation logging is best-effort and must not break primary acquisition recording.
- async shutdown()[source]#
Runs when the Unit terminates.
This is called from within the same process this unit will live in. This lifecycle hook can be overridden. It can be run as async functions by simply adding the async keyword when overriding.
This method is where you should clean up resources and perform any necessary shutdown procedures.
- Return type:
- class NWBSinkConsumer(*args, settings=None, **kwargs)[source]#
Bases:
BaseStatefulConsumer[NWBSinkSettings,AxisArray,NWBSinkState]- Parameters:
settings (SettingsType)
- __init__(*args, settings=None, **kwargs)[source]#
- Parameters:
settings (NWBSinkSettings | None)
- get_session_datetime(try_t0=None)[source]#
- Retrieve session datetime. If it does not already exist, set it with try_t0 if provided
and clock is understood, else with datetime.now.
- get_session_timestamp(try_t0=None)[source]#
Retrieve session timestamp. If it does not already exist, set it with try_t0 if provided.
- close(write=False, log=True)[source]#
Close the file. This will also delete the file if it is empty.
- Parameters:
write – Set True to write the file to disk before closing.
log – Set True to log the closing and deletion of the file. This must be kept False when calling from __del__.
- Return type:
- toggle_recording(recording=None)[source]#
Deprecated. Send a
NWBSinkSettingsupdate with the desiredrecordingvalue instead — the update is routed throughupdate_settings()and takes effect on the next message.- Parameters:
recording (bool | None)
- write_annotation(table_name, timestamp, data)[source]#
Append one row to the named
AnnotationSeries, creating it if needed.timestampis wall-clock seconds. The value stored in the file istimestamp - start_timestamponce a data message has setstart_timestamp(the file-relative convention NWB expects); before then, the wall-clock value is passed through unmodified — the sink has no baseline to subtract against, and inventing one (e.g. fromdatetime.now()) would silently desync from the eventual data anchor. Annotations written before any data arrives therefore carry whatever baseline their producer chose; downstream readers should treat them accordingly.
- class NWBSinkSettings(filepath, overwrite_old=False, axis='time', recording=True, inc_clock=ReferenceClockType.SYSTEM, meta_yaml=None, split_bytes=0, expected_series=None)[source]#
Bases:
Settings- Parameters:
- inc_clock: ReferenceClockType = 'system'#
- __init__(filepath, overwrite_old=False, axis='time', recording=True, inc_clock=ReferenceClockType.SYSTEM, meta_yaml=None, split_bytes=0, expected_series=None)#
- class SeriesState(shape, data=None, ts=None, bytes_written=0)[source]#
Bases:
objectPer-stream bookkeeping for one NWB series.
shapeis the trailing (non-time) shape used byNWBSinkConsumer._check_msg_consistency().dataandtshold the liveh5py.Dataset(or pynwb container) references used byNWBSinkConsumer._process()to append rows.bytes_writtentracks cumulative append size for the file-split threshold.