|
| 1 | +""" |
| 2 | +.. _time_intervals: |
| 3 | +
|
| 4 | +Annotating Time Intervals |
| 5 | +========================= |
| 6 | +
|
| 7 | +Annotating events in time is a common need in neuroscience, e.g. to describes epochs, trials, and |
| 8 | +invalid times during an experimental session. NWB supports annotation of time intervals via the |
| 9 | +:py:class:`~pynwb.epoch.TimeIntervals` type. The :py:class:`~pynwb.epoch.TimeIntervals` type is |
| 10 | +a :py:class:`~hdmf.common.table.DynamicTable` with the following columns: |
| 11 | +
|
| 12 | +1. :py:meth:`~pynwb.epoch.TimeIntervals.start_time` and :py:meth:`~pynwb.epoch.TimeIntervals.stop_time` |
| 13 | + describe the start and stop times of intervals as floating point offsets in seconds relative to the |
| 14 | + :py:meth:`~pynwb.file.NWBFile.timestamps_reference_time` of the file. In addition, |
| 15 | +2. :py:class:`~pynwb.epoch.TimeIntervals.tags` is an optional, indexed column used to associate user-defined string |
| 16 | + tags with intervals (0 or more tags per time interval) |
| 17 | +3. :py:class:`~pynwb.epoch.TimeIntervals.timeseries` is an optional, indexed |
| 18 | + :py:class:`~pynwb.base.TimeSeriesReferenceVectorData` column to map intervals directly to ranges in select, |
| 19 | + relevant :py:class:`~pynwb.base.TimeSeries` (0 or more per time interval) |
| 20 | +4. as a :py:class:`~hdmf.common.table.DynamicTable` user may add additional columns to |
| 21 | + :py:meth:`~pynwb.epoch.TimeIntervals` via :py:class:`~hdmf.common.table.DynamicTable.add_column` |
| 22 | +
|
| 23 | +
|
| 24 | +.. hint:: :py:meth:`~pynwb.epoch.TimeIntervals` is intended for storing general annotations of time ranges. |
| 25 | + Depending on the application (e.g., when intervals are generated by data acquisition or automatic |
| 26 | + data processing), it can be useful to describe intervals (or instantaneous events) in time |
| 27 | + as :py:class:`~pynwb.base.TimeSeries`. NWB provides several types for this purposes, e.g., |
| 28 | + :py:class:`~pynwb.misc.IntervalSeries`, :py:class:`~pynwb.behavior.BehavioralEpochs`, |
| 29 | + :py:class:`~pynwb.behavior.BehavioralEvents`, :py:class:`~pynwb.ecephys.EventDetection`, or |
| 30 | + :py:class:`~pynwb.ecephys.SpikeEventSeries`. |
| 31 | +
|
| 32 | +""" |
| 33 | + |
| 34 | +#################### |
| 35 | +# Setup: Creating an example NWB file for the tutorial |
| 36 | +# ---------------------------------------------------- |
| 37 | +# |
| 38 | + |
| 39 | +# sphinx_gallery_thumbnail_path = 'figures/gallery_thumbnails_timeintervals.png' |
| 40 | +from datetime import datetime |
| 41 | +from dateutil.tz import tzlocal |
| 42 | +from pynwb import NWBFile |
| 43 | +from pynwb import TimeSeries |
| 44 | +import numpy as np |
| 45 | + |
| 46 | +# create the NWBFile |
| 47 | +nwbfile = NWBFile(session_description='NWBFile to illustrate TimeIntervals basics', |
| 48 | + identifier='NWB123', |
| 49 | + session_start_time=datetime(2017, 4, 3, 11, tzinfo=tzlocal()), |
| 50 | + file_create_date=datetime(2017, 4, 15, 12, tzinfo=tzlocal())) |
| 51 | +# create some example TimeSeries |
| 52 | +test_ts = TimeSeries(name='series1', |
| 53 | + data=np.arange(1000), |
| 54 | + unit='m', |
| 55 | + timestamps=np.linspace(0.5, 601, 1000)) |
| 56 | +rate_ts = TimeSeries(name='series2', |
| 57 | + data=np.arange(600), |
| 58 | + unit='V', |
| 59 | + starting_time=0.0, rate=1.0) |
| 60 | +# Add the TimeSeries to the file |
| 61 | +nwbfile.add_acquisition(test_ts) |
| 62 | +nwbfile.add_acquisition(rate_ts) |
| 63 | + |
| 64 | +#################### |
| 65 | +# Adding Time Intervals to a NWBFile |
| 66 | +# ---------------------------------- |
| 67 | +# |
| 68 | +# NWB provides a set of pre-defined :py:class:`~pynwb.epoch.TimeIntervals` |
| 69 | +# tables for :py:meth:`~pynwb.file.NWBFile.epochs`, :py:meth:`~pynwb.file.NWBFile.trials`, and |
| 70 | +# :py:meth:`~pynwb.file.NWBFile.invalid_times`. |
| 71 | +# |
| 72 | +# Trials |
| 73 | +# ^^^^^^ |
| 74 | +# |
| 75 | +# Trials can be added to an NWB file using the methods :py:meth:`~pynwb.file.NWBFile.add_trial` |
| 76 | +# By default, NWBFile only requires trial :py:meth:`~pynwb.file.NWBFile.add_trial.start_time` |
| 77 | +# and :py:meth:`~pynwb.file.NWBFile.add_trial.end_time`. The :py:meth:`~pynwb.file.NWBFile.add_trial.tags` |
| 78 | +# and :py:meth:`~pynwb.file.NWBFile.add_trial.timeseries` are optional. For |
| 79 | +# :py:meth:`~pynwb.file.NWBFile.add_trial.timeseries` we only need to supply the :py:class:`~pynwb.base.TimeSeries`. |
| 80 | +# PyNWB automatically calculates the corresponding index range (described by ``idx_start`` and ``count``) for |
| 81 | +# the supplied :py:class:`~pynwb.base.TimeSeries based on the given ``start_time`` and ``stop_time`` and |
| 82 | +# the :py:meth:`~pynwb.base.TimeSeries.timestamps` (or :py:class:`~pynwb.base.TimeSeries.starting_time` |
| 83 | +# and :py:meth:`~pynwb.base.TimeSeries.rate`) of the given :py:class:`~pynwb.base.TimeSeries`. |
| 84 | +# |
| 85 | +# Additional columns can be added using :py:meth:`~pynwb.file.NWBFile.add_trial_column`. This method takes a name |
| 86 | +# for the column and a description of what the column stores. You do not need to supply data |
| 87 | +# type, as this will inferred. Once all columns have been added, trial data can be populated using |
| 88 | +# :py:meth:`~pynwb.file.NWBFile.add_trial`. |
| 89 | +# |
| 90 | +# Lets add an additional column and some trial data with tags and timeseries references. |
| 91 | + |
| 92 | +nwbfile.add_trial_column(name='stim', description='the visual stimuli during the trial') |
| 93 | + |
| 94 | +nwbfile.add_trial(start_time=0.0, stop_time=2.0, stim='dog', |
| 95 | + tags=['animal'], timeseries=[test_ts, rate_ts]) |
| 96 | +nwbfile.add_trial(start_time=3.0, stop_time=5.0, stim='mountain', |
| 97 | + tags=['landscape'], timeseries=[test_ts, rate_ts]) |
| 98 | +nwbfile.add_trial(start_time=6.0, stop_time=8.0, stim='desert', |
| 99 | + tags=['landscape'], timeseries=[test_ts, rate_ts]) |
| 100 | +nwbfile.add_trial(start_time=9.0, stop_time=11.0, stim='tree', |
| 101 | + tags=['landscape', 'plant'], timeseries=[test_ts, rate_ts]) |
| 102 | +nwbfile.add_trial(start_time=12.0, stop_time=14.0, stim='bird', |
| 103 | + tags=['animal'], timeseries=[test_ts, rate_ts]) |
| 104 | +nwbfile.add_trial(start_time=15.0, stop_time=17.0, stim='flower', |
| 105 | + tags=['animal'], timeseries=[test_ts, rate_ts]) |
| 106 | + |
| 107 | +#################### |
| 108 | +# Epochs |
| 109 | +# ^^^^^^ |
| 110 | +# |
| 111 | +# Similarly, epochs can be added to an NWB file using the method :py:meth:`~pynwb.file.NWBFile.add_epoch` and |
| 112 | +# :py:meth:`~pynwb.file.NWBFile.add_epoch_column`. |
| 113 | + |
| 114 | +nwbfile.add_epoch(2.0, 4.0, ['first', 'example'], [test_ts, ]) |
| 115 | +nwbfile.add_epoch(6.0, 8.0, ['second', 'example'], [test_ts, ]) |
| 116 | + |
| 117 | +#################### |
| 118 | +# Invalid Times |
| 119 | +# ^^^^^^^^^^^^^ |
| 120 | +# |
| 121 | +# Similarly, invalid times can be added using the method :py:meth:`~pynwb.file.NWBFile.add_invalid_time_interval` and |
| 122 | +# :py:meth:`~pynwb.file.NWBFile.add_invalid_times_column`. |
| 123 | + |
| 124 | +nwbfile.add_epoch(2.0, 4.0, ['first', 'example'], [test_ts, ]) |
| 125 | +nwbfile.add_epoch(6.0, 8.0, ['second', 'example'], [test_ts, ]) |
| 126 | + |
| 127 | +#################### |
| 128 | +# Custom Time Intervals |
| 129 | +# ^^^^^^^^^^^^^^^^^^^^^ |
| 130 | +# |
| 131 | +# To define custom, experiment-specific :py:class:`~pynwb.epoch.TimeIntervals` we can wither add them |
| 132 | +# either: 1) when creating the :py:class:`~pynwb.file.NWBFile` by defining the |
| 133 | +# :py:meth:`~pynwb.file.NWBFile.__init__.intervals` constructor argument or 2) via the |
| 134 | +# :py:meth:`~pynwb.file.NWBFile.add_time_intervals` or :py:meth:`~pynwb.file.NWBFile.create_time_intervals` |
| 135 | +# after the :py:class:`~pynwb.file.NWBFile` has been created. |
| 136 | +# |
| 137 | + |
| 138 | +from pynwb.epoch import TimeIntervals |
| 139 | + |
| 140 | +sleep_stages = TimeIntervals( |
| 141 | + name="sleep_stages", |
| 142 | + description="intervals for each sleep stage as determined by EEG", |
| 143 | +) |
| 144 | + |
| 145 | +sleep_stages.add_column(name="stage", description="stage of sleep") |
| 146 | +sleep_stages.add_column(name="confidence", description="confidence in stage (0-1)") |
| 147 | + |
| 148 | +sleep_stages.add_row(start_time=0.3, stop_time=0.5, stage=1, confidence=.5) |
| 149 | +sleep_stages.add_row(start_time=0.7, stop_time=0.9, stage=2, confidence=.99) |
| 150 | +sleep_stages.add_row(start_time=1.3, stop_time=3.0, stage=3, confidence=0.7) |
| 151 | + |
| 152 | +_ = nwbfile.add_time_intervals(sleep_stages) |
| 153 | + |
| 154 | + |
| 155 | +#################### |
| 156 | +# Accessing Time Intervals |
| 157 | +# ------------------------ |
| 158 | +# |
| 159 | +# We can access the predefined :py:class:`~pynwb.epoch.TimeIntervals` tables via the corresponding |
| 160 | +# :py:meth:`~pynwb.file.NWBFile.epochs`, :py:meth:`~pynwb.file.NWBFile.trials`, and |
| 161 | +# :py:meth:`~pynwb.file.NWBFile.invalid_times` properties and for custom :py:class:`~pynwb.epoch.TimeIntervals` |
| 162 | +# via the :py:meth:`~pynwb.file.NWBFile.get_time_intervals` method. E.g.: |
| 163 | + |
| 164 | +_ = nwbfile.intervals |
| 165 | +_ = nwbfile.get_time_intervals('sleep_stages') |
| 166 | + |
| 167 | + |
| 168 | +#################### |
| 169 | +# Like any :py:class:`~hdmf.common.table.DynamicTable`, we can conveniently convert any |
| 170 | +# :py:class:`~pynwb.epoch.TimeIntervals` table to a ``pandas.DataFrame`` via |
| 171 | +# :py:meth:`~hdmf.common.table.DynamicTable.to_dataframe`, such as: |
| 172 | + |
| 173 | +nwbfile.trials.to_dataframe() |
| 174 | + |
| 175 | +#################### |
| 176 | +# This approach makes it easy to query the data to, e.g., locate all time intervals within a certain time range |
| 177 | + |
| 178 | +trials_df = nwbfile.trials.to_dataframe() |
| 179 | +trials_df.query('(start_time > 2.0) & (stop_time < 9.0)') |
| 180 | + |
| 181 | +#################### |
| 182 | +# Accessing referenced TimeSeries |
| 183 | +# ------------------------------- |
| 184 | +# |
| 185 | +# As mentioned earlier, the ``timeseries`` column is defined by a :py:class:`~pynwb.base.TimeSeriesReferenceVectorData` |
| 186 | +# which stores references to the corresponding ranges in :py:class:`~pynwb.base.TimeSeries`. Individual references |
| 187 | +# to :py:class:`~pynwb.base.TimeSeries` are described via :py:class:`~pynwb.base.TimeSeriesReference` tuples |
| 188 | +# with the :py:class:`~pynwb.base.TimeSeriesReference.idx_start`, :py:class:`~pynwb.base.TimeSeriesReference.count`, |
| 189 | +# and :py:class:`~pynwb.base.TimeSeriesReference.timeseries`. |
| 190 | +# Using :py:class:`~pynwb.base.TimeSeriesReference` we can easily access the relevant |
| 191 | +# :py:meth:`~pynwb.base.TimeSeriesReference.data` and :py:meth:`~pynwb.base.TimeSeriesReference.timestamps` |
| 192 | +# for the corresponding time range from the :py:class:`~pynwb.base.TimeSeries`. |
| 193 | + |
| 194 | +# Get a single example TimeSeriesReference from the trials table |
| 195 | +example_tsr = nwbfile.trials['timeseries'][0][0] |
| 196 | + |
| 197 | +# Get the data values from the timeseries. This is a shorthand for: |
| 198 | +# _ = example_tsr.timeseries.data[example_tsr.idx_start: (example_tsr.idx_start + example_tsr.count)] |
| 199 | +_ = example_tsr.data |
| 200 | + |
| 201 | +# Get the timestamps. Timestamps are either loaded from the TimeSeries or |
| 202 | +# computed from the starting_time and rate |
| 203 | +example_tsr.timestamps |
| 204 | + |
| 205 | +#################### |
| 206 | +# Using :py:class:`~pynwb.base.TimeSeriesReference.isvalid` we can further check if the reference is valid. |
| 207 | +# A :py:class:`~pynwb.base.TimeSeriesReference` is defined as invalid if both |
| 208 | +# :py:class:`~pynwb.base.TimeSeriesReference.idx_start`, :py:class:`~pynwb.base.TimeSeriesReference.count` are |
| 209 | +# set to ``-1``. :py:class:`~pynwb.base.TimeSeriesReference.isvalid` further also checks that the indicated |
| 210 | +# index range and types are valid, raising ``IndexError`` and ``TypeError`` respectively, if bad |
| 211 | +# :py:class:`~pynwb.base.TimeSeriesReference.idx_start`, :py:class:`~pynwb.base.TimeSeriesReference.count` or |
| 212 | +# :py:class:`~pynwb.base.TimeSeriesReference.timeseries` are found. |
| 213 | + |
| 214 | +example_tsr.isvalid() |
| 215 | + |
| 216 | +#################### |
| 217 | +# Adding TimeSeries references to other tables |
| 218 | +# -------------------------------------------- |
| 219 | +# |
| 220 | +# Since :py:class:`~pynwb.base.TimeSeriesReferenceVectorData` is a regular :py:class:`~hdmf.common.table.VectoData` |
| 221 | +# type, we can use it to add references to intervals in :py:class:`~pynwb.base.TimeSeries` to any |
| 222 | +# :py:class:`~hdmf.common.table.DynamicTable`. In the :py:class:`~pynwb.icephys.IntracellularRecordingTable`, e.g., |
| 223 | +# it is used to reference the recording of the stimulus and response associated with a particular intracellular |
| 224 | +# electrophysiology recording. |
| 225 | +# |
| 226 | + |
| 227 | + |
| 228 | +#################### |
| 229 | +# Reading/Writing TimeIntervals to file |
| 230 | +# ------------------------------------- |
| 231 | +# |
| 232 | +# Reading and writing the data is as usual: |
| 233 | + |
| 234 | +from pynwb import NWBHDF5IO |
| 235 | +# write the file |
| 236 | +with NWBHDF5IO('example_timeintervals_file.nwb', 'w') as io: |
| 237 | + io.write(nwbfile) |
| 238 | +# read the file |
| 239 | +io = NWBHDF5IO('example_timeintervals_file.nwb', 'r') |
| 240 | +nwbfile_in = io.read() |
| 241 | +# plot the sleep stages TimeIntervals table |
| 242 | +nwbfile.get_time_intervals('sleep_stages').to_dataframe() |
0 commit comments