|
| 1 | +""" |
| 2 | +.. _modifying_data: |
| 3 | +
|
| 4 | +Adding/removing containers from an NWB file |
| 5 | +============================================ |
| 6 | +
|
| 7 | +This tutorial explains how to add and remove containers from an existing NWB file and either write the data back to the |
| 8 | +same file or export the data to a new file. |
| 9 | +""" |
| 10 | + |
| 11 | +############################################################################### |
| 12 | +# Adding objects to an NWB file in read/write mode |
| 13 | +# ---------------------------------------------------- |
| 14 | +# PyNWB supports adding container objects to an existing NWB file - that is, reading data from an NWB file, adding a |
| 15 | +# container object, such as a new :py:class:`~pynwb.base.TimeSeries` object, and writing the modified |
| 16 | +# :py:class:`~pynwb.file.NWBFile` back to the same file path on disk. To do so: |
| 17 | +# |
| 18 | +# 1. open the file with an :py:class:`~pynwb.NWBHDF5IO` object in read/write mode (``mode='r+'`` or ``mode='a'``) |
| 19 | +# 2. read the :py:class:`~pynwb.file.NWBFile` |
| 20 | +# 3. add container objects to the :py:class:`~pynwb.file.NWBFile` object |
| 21 | +# 4. write the modified :py:class:`~pynwb.file.NWBFile` using the same :py:class:`~pynwb.NWBHDF5IO` object |
| 22 | +# |
| 23 | +# For example: |
| 24 | + |
| 25 | +from pynwb import NWBFile, NWBHDF5IO, TimeSeries |
| 26 | +import datetime |
| 27 | +import numpy as np |
| 28 | + |
| 29 | +# first, write a test NWB file |
| 30 | +nwbfile = NWBFile( |
| 31 | + session_description='demonstrate adding to an NWB file', |
| 32 | + identifier='NWB123', |
| 33 | + session_start_time=datetime.datetime.now(datetime.timezone.utc), |
| 34 | +) |
| 35 | + |
| 36 | +filename = 'nwbfile.nwb' |
| 37 | +with NWBHDF5IO(filename, 'w') as io: |
| 38 | + io.write(nwbfile) |
| 39 | + |
| 40 | +# open the NWB file in r+ mode |
| 41 | +with NWBHDF5IO(filename, 'r+') as io: |
| 42 | + read_nwbfile = io.read() |
| 43 | + |
| 44 | + # create a TimeSeries and add it to the file under the acquisition group |
| 45 | + data = list(range(100, 200, 10)) |
| 46 | + timestamps = np.arange(10, dtype=np.float) |
| 47 | + test_ts = TimeSeries( |
| 48 | + name='test_timeseries', |
| 49 | + data=data, |
| 50 | + unit='m', |
| 51 | + timestamps=timestamps |
| 52 | + ) |
| 53 | + read_nwbfile.add_acquisition(test_ts) |
| 54 | + |
| 55 | + # write the modified NWB file |
| 56 | + io.write(read_nwbfile) |
| 57 | + |
| 58 | +# confirm the file contains the new TimeSeries in acquisition |
| 59 | +with NWBHDF5IO(filename, 'r') as io: |
| 60 | + read_nwbfile = io.read() |
| 61 | + print(read_nwbfile) |
| 62 | + |
| 63 | +############################################################################### |
| 64 | +# .. note:: |
| 65 | +# |
| 66 | +# You cannot remove objects from an NWB file using the above method. |
| 67 | + |
| 68 | +############################################################################### |
| 69 | +# Modifying an NWB file in this way has limitations. The destination file path must be the same as the source |
| 70 | +# file path, and it is not possible to remove objects from an NWB file. You can use the |
| 71 | +# :py:meth:`NWBHDF5IO.export <pynwb.NWBHDF5IO.export>` method, detailed below, to modify an NWB file in these ways. |
| 72 | +# |
| 73 | +# .. warning:: |
| 74 | +# |
| 75 | +# NWB datasets that have been written to disk are read as :py:class:`h5py.Dataset <h5py.Dataset>` objects. |
| 76 | +# Directly modifying the data in these :py:class:`h5py.Dataset <h5py.Dataset>` objects immediately |
| 77 | +# modifies the data on disk |
| 78 | +# (the :py:meth:`NWBHDF5IO.write <pynwb.NWBHDF5IO.write>` method does not need to be called and the |
| 79 | +# :py:class:`~pynwb.NWBHDF5IO` instance does not need to be closed). Directly modifying datasets in this way |
| 80 | +# can lead to files that do not validate or cannot be opened, so take caution when using this method. |
| 81 | +# Note: only chunked datasets or datasets with ``maxshape`` set can be resized. |
| 82 | +# See the `h5py chunked storage documentation <https://docs.h5py.org/en/stable/high/dataset.html#chunked-storage>`_ |
| 83 | +# for more details. |
| 84 | + |
| 85 | +############################################################################### |
| 86 | +# .. note:: |
| 87 | +# |
| 88 | +# It is not possible to modify the attributes (fields) of an NWB container in memory. |
| 89 | + |
| 90 | +############################################################################### |
| 91 | +# Exporting a written NWB file to a new file path |
| 92 | +# --------------------------------------------------- |
| 93 | +# Use the :py:meth:`NWBHDF5IO.export <pynwb.NWBHDF5IO.export>` method to read data to an existing NWB file, |
| 94 | +# modify the data, and write the modified data to a new file path. Modifications to the data can be additions or |
| 95 | +# removals of objects, such as :py:class:`~pynwb.base.TimeSeries` objects. This is especially useful if you |
| 96 | +# have raw data and processed data in the same NWB file and you want to create a new NWB file with all of the |
| 97 | +# contents of the original file except for the raw data for sharing with collaborators. |
| 98 | +# |
| 99 | +# To remove existing containers, use the :py:class:`~hdmf.utils.LabelledDict.pop` method on any |
| 100 | +# :py:class:`~hdmf.utils.LabelledDict` object, such as ``NWBFile.acquisition``, ``NWBFile.processing``, |
| 101 | +# ``NWBFile.analysis``, ``NWBFile.processing``, ``NWBFile.scratch``, ``NWBFile.devices``, ``NWBFile.stimulus``, |
| 102 | +# ``NWBFile.stimulus_template``, ``NWBFile.electrode_groups``, ``NWBFile.imaging_planes``, |
| 103 | +# ``NWBFile.icephys_electrodes``, ``NWBFile.ogen_sites``, ``NWBFile.lab_meta_data``, |
| 104 | +# and :py:class:`~pynwb.base.ProcessingModule` objects. |
| 105 | +# |
| 106 | +# For example: |
| 107 | + |
| 108 | +# first, create a test NWB file with a TimeSeries in the acquisition group |
| 109 | +nwbfile = NWBFile( |
| 110 | + session_description='demonstrate export of an NWB file', |
| 111 | + identifier='NWB123', |
| 112 | + session_start_time=datetime.datetime.now(datetime.timezone.utc), |
| 113 | +) |
| 114 | +data1 = list(range(100, 200, 10)) |
| 115 | +timestamps1 = np.arange(10, dtype=np.float) |
| 116 | +test_ts1 = TimeSeries( |
| 117 | + name='test_timeseries1', |
| 118 | + data=data1, |
| 119 | + unit='m', |
| 120 | + timestamps=timestamps1 |
| 121 | +) |
| 122 | +nwbfile.add_acquisition(test_ts1) |
| 123 | + |
| 124 | +# then, create a processing module for processed behavioral data |
| 125 | +nwbfile.create_processing_module( |
| 126 | + name='behavior', |
| 127 | + description='processed behavioral data' |
| 128 | +) |
| 129 | +data2 = list(range(100, 200, 10)) |
| 130 | +timestamps2 = np.arange(10, dtype=np.float) |
| 131 | +test_ts2 = TimeSeries( |
| 132 | + name='test_timeseries2', |
| 133 | + data=data2, |
| 134 | + unit='m', |
| 135 | + timestamps=timestamps2 |
| 136 | +) |
| 137 | +nwbfile.processing['behavior'].add(test_ts2) |
| 138 | + |
| 139 | +# write these objects to an NWB file |
| 140 | +filename = 'nwbfile.nwb' |
| 141 | +with NWBHDF5IO(filename, 'w') as io: |
| 142 | + io.write(nwbfile) |
| 143 | + |
| 144 | +# read the written file |
| 145 | +export_filename = 'exported_nwbfile.nwb' |
| 146 | +with NWBHDF5IO(filename, mode='r') as read_io: |
| 147 | + read_nwbfile = read_io.read() |
| 148 | + |
| 149 | + # add a new TimeSeries to the behavior processing module |
| 150 | + data3 = list(range(100, 200, 10)) |
| 151 | + timestamps3 = np.arange(10, dtype=np.float) |
| 152 | + test_ts3 = TimeSeries( |
| 153 | + name='test_timeseries3', |
| 154 | + data=data3, |
| 155 | + unit='m', |
| 156 | + timestamps=timestamps3 |
| 157 | + ) |
| 158 | + read_nwbfile.processing['behavior'].add(test_ts3) |
| 159 | + |
| 160 | + # use the pop method to remove the original TimeSeries from the acquisition group |
| 161 | + read_nwbfile.acquisition.pop('test_timeseries1') |
| 162 | + |
| 163 | + # use the pop method to remove a TimeSeries from a processing module |
| 164 | + read_nwbfile.processing['behavior'].data_interfaces.pop('test_timeseries2') |
| 165 | + |
| 166 | + # call the export method to write the modified NWBFile instance to a new file path |
| 167 | + # the original file is not modified |
| 168 | + with NWBHDF5IO(export_filename, mode='w') as export_io: |
| 169 | + export_io.export(src_io=read_io, nwbfile=read_nwbfile) |
| 170 | + |
| 171 | +# confirm the exported file does not contain TimeSeries with names 'test_timeseries1' or 'test_timeseries2' |
| 172 | +# but does contain a new TimeSeries in processing['behavior'] with name 'test_timeseries3' |
| 173 | +with NWBHDF5IO(export_filename, 'r') as io: |
| 174 | + read_nwbfile = io.read() |
| 175 | + print(read_nwbfile) |
| 176 | + print(read_nwbfile.processing['behavior']) |
| 177 | + |
| 178 | +############################################################################### |
| 179 | +# .. note:: |
| 180 | +# |
| 181 | +# :py:class:`~pynwb.epoch.TimeIntervals` objects, such as ``NWBFile.epochs``, ``NWBFile.trials``, |
| 182 | +# ``NWBFile.invalid_times``, and custom :py:class:`~pynwb.epoch.TimeIntervals` objects cannot be |
| 183 | +# removed (popped) from ``NWBFile.intervals``. |
| 184 | + |
| 185 | +############################################################################### |
| 186 | +# .. warning:: |
| 187 | +# |
| 188 | +# Removing an object from an NWBFile may break links and references within the file and across files. |
| 189 | +# This is analogous to having shortcuts/aliases to a file on your filesystem and then deleting the file. |
| 190 | +# Extra caution should be taken when removing heavily referenced items such as |
| 191 | +# :py:class:`~pynwb.device.Device` objects, |
| 192 | +# :py:class:`~pynwb.ecephys.ElectrodeGroup` objects, the electrodes table, and the |
| 193 | +# :py:class:`~pynwb.ophys.PlaneSegmentation` table. |
| 194 | + |
| 195 | +############################################################################### |
| 196 | +# Exporting with new object IDs |
| 197 | +# --------------------------------- |
| 198 | +# When exporting a read NWB file to a new file path, the object IDs within the original NWB file will be copied to the |
| 199 | +# new file. To make the exported NWB file contain a new set of object IDs, call |
| 200 | +# :py:meth:`~hdmf.container.AbstractContainer.generate_new_id` on your :py:class:`~pynwb.file.NWBFile` object. |
| 201 | +# This will generate a new object ID for the :py:class:`~pynwb.file.NWBFile` object and all of the objects within |
| 202 | +# the NWB file. |
| 203 | + |
| 204 | +export_filename = 'exported_nwbfile.nwb' |
| 205 | +with NWBHDF5IO(filename, mode='r') as read_io: |
| 206 | + read_nwbfile = read_io.read() |
| 207 | + read_nwbfile.generate_new_id() |
| 208 | + |
| 209 | + with NWBHDF5IO(export_filename, mode='w') as export_io: |
| 210 | + export_io.export(src_io=read_io, nwbfile=read_nwbfile) |
| 211 | + |
| 212 | +############################################################################### |
| 213 | +# More information about export |
| 214 | +# --------------------------------- |
| 215 | +# For more information about the export functionality, see https://hdmf.readthedocs.io/en/latest/export.html |
0 commit comments