Skip to content

Commit 18d4cad

Browse files
weiglszonjarly
andauthored
Avoid raising data timestamps length mismatch warning with external file (#1486)
* extend check criteria of data-timestamps mismatch warming * add unittest to check new behavior * move check to private method * overwrite timeseries check method * remove unused import * remove unused import * trigger time series dimension warning when external_file is None * change dimension check to class method * refactor tests * flake8 * Update to fix property calls and add docs * Fix check * Fix check * Fix check * Fix test with warning * update CHANGELOG.md * test warning is not raised with rate specified Co-authored-by: Ryan Ly <[email protected]>
1 parent cf0ee6d commit 18d4cad

File tree

5 files changed

+93
-20
lines changed

5 files changed

+93
-20
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
- Added support for HDMF 3.3.1. This is now the minimum version of HDMF supported. Importantly, HDMF 3.3 introduces
2626
warnings when the constructor of a class mapped to an HDMF-common data type or an autogenerated data type class
2727
is passed positional arguments instead of all keyword arguments. @rly (#1484)
28+
- Moved logic that checks the 0th dimension of TimeSeries data equals the length of timestamps to a private method in the
29+
``TimeSeries`` class. This is to avoid raising a warning when an ImageSeries is used with external file. @weiglszonja (#1486)
2830

2931
### Documentation and tutorial enhancements:
3032
- Added tutorial on annotating data via ``TimeIntervals``. @oruebel (#1390)

src/pynwb/base.py

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -166,24 +166,6 @@ def __init__(self, **kwargs):
166166
for key, val in args_to_set.items():
167167
setattr(self, key, val)
168168

169-
data_shape = get_data_shape(data=args_to_process["data"], strict_no_data_load=True)
170-
timestamps_shape = get_data_shape(data=args_to_process["timestamps"], strict_no_data_load=True)
171-
if (
172-
# check that the shape is known
173-
data_shape is not None and timestamps_shape is not None
174-
175-
# check for scalars. Should never happen
176-
and (len(data_shape) > 0 and len(timestamps_shape) > 0)
177-
178-
# check that the length of the first dimension is known
179-
and (data_shape[0] is not None and timestamps_shape[0] is not None)
180-
181-
# check that the data and timestamps match
182-
and (data_shape[0] != timestamps_shape[0])
183-
):
184-
warn("Length of data does not match length of timestamps. Your data may be transposed. Time should be on "
185-
"the 0th dimension")
186-
187169
data = args_to_process['data']
188170
self.fields['data'] = data
189171
if isinstance(data, TimeSeries):
@@ -207,6 +189,29 @@ def __init__(self, **kwargs):
207189
else:
208190
raise TypeError("either 'timestamps' or 'rate' must be specified")
209191

192+
if not self._check_time_series_dimension():
193+
warn("Length of data does not match length of timestamps. Your data may be transposed. Time should be on "
194+
"the 0th dimension")
195+
196+
def _check_time_series_dimension(self):
197+
"""Check that the 0th dimension of data equals the length of timestamps, when applicable.
198+
"""
199+
if self.timestamps is None:
200+
return True
201+
202+
data_shape = get_data_shape(data=self.fields["data"], strict_no_data_load=True)
203+
timestamps_shape = get_data_shape(data=self.fields["timestamps"], strict_no_data_load=True)
204+
205+
# skip check if shape of data or timestamps cannot be computed
206+
if data_shape is None or timestamps_shape is None:
207+
return True
208+
209+
# skip check if length of the first dimension is not known
210+
if data_shape[0] is None or timestamps_shape[0] is None:
211+
return True
212+
213+
return data_shape[0] == timestamps_shape[0]
214+
210215
@property
211216
def num_samples(self):
212217
''' Tries to return the number of data samples. If this cannot be assessed, returns None.

src/pynwb/image.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,36 @@ def __init__(self, **kwargs):
7272
if unit is None:
7373
kwargs['unit'] = ImageSeries.DEFAULT_UNIT
7474

75-
# TODO catch warning when default data is used and timestamps are provided
7675
super().__init__(**kwargs)
7776

7877
if args_to_set["external_file"] is None:
7978
args_to_set["starting_frame"] = None # overwrite starting_frame
8079
for key, val in args_to_set.items():
8180
setattr(self, key, val)
8281

82+
if not self._check_image_series_dimension():
83+
warnings.warn(
84+
"Length of data does not match length of timestamps. "
85+
"Your data may be transposed. Time should be on the 0th dimension"
86+
)
87+
88+
def _check_time_series_dimension(self):
89+
"""Override _check_time_series_dimension to do nothing.
90+
The _check_image_series_dimension method will be called instead.
91+
"""
92+
return True
93+
94+
def _check_image_series_dimension(self):
95+
"""Check that the 0th dimension of data equals the length of timestamps, when applicable.
96+
97+
ImageSeries objects can have an external file instead of data stored. The external file cannot be
98+
queried for the number of frames it contains, so this check will return True when an external file
99+
is provided. Otherwise, this function calls the parent class' _check_time_series_dimension method.
100+
"""
101+
if self.external_file is not None:
102+
return True
103+
return super()._check_time_series_dimension()
104+
83105
@property
84106
def bits_per_pixel(self):
85107
return self.fields.get('bits_per_pixel')

tests/integration/hdf5/test_ophys.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ class TestTwoPhotonSeriesIO(AcquisitionH5IOMixin, TestCase):
140140
def setUpContainer(self):
141141
""" Return the test TwoPhotonSeries to read/write """
142142
self.device, self.optical_channel, self.imaging_plane = make_imaging_plane()
143-
data = [[[1., 1.] * 2] * 2]
143+
data = np.ones((10, 2, 2))
144144
timestamps = list(map(lambda x: x/10, range(10)))
145145
fov = [2.0, 2.0, 5.0]
146146
ret = TwoPhotonSeries(

tests/unit/test_image.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import warnings
2+
13
import numpy as np
24

35
from pynwb import TimeSeries
@@ -73,6 +75,48 @@ def test_external_file_no_unit(self):
7375
)
7476
self.assertEqual(iS.unit, ImageSeries.DEFAULT_UNIT)
7577

78+
def test_dimension_warning(self):
79+
"""Test that a warning is raised when the dimensions of the data are not the
80+
same as the dimensions of the timestamps."""
81+
msg = (
82+
"Length of data does not match length of timestamps. Your data may be "
83+
"transposed. Time should be on the 0th dimension"
84+
)
85+
with self.assertWarnsWith(UserWarning, msg):
86+
ImageSeries(
87+
name='test_iS',
88+
data=np.ones((3, 3, 3)),
89+
unit='Frames',
90+
starting_frame=[0],
91+
timestamps=[1, 2, 3, 4]
92+
)
93+
94+
def test_dimension_warning_external_file_with_timestamps(self):
95+
"""Test that a warning is not raised when external file is used with timestamps."""
96+
with warnings.catch_warnings(record=True) as w:
97+
ImageSeries(
98+
name='test_iS',
99+
external_file=['external_file'],
100+
format='external',
101+
unit='Frames',
102+
starting_frame=[0],
103+
timestamps=[1, 2, 3, 4]
104+
)
105+
self.assertEqual(w, [])
106+
107+
def test_dimension_warning_external_file_with_rate(self):
108+
"""Test that a warning is not raised when external file is used with rate."""
109+
with warnings.catch_warnings(record=True) as w:
110+
ImageSeries(
111+
name='test_iS',
112+
external_file=['external_file'],
113+
format='external',
114+
unit='Frames',
115+
starting_frame=[0],
116+
rate=0.2,
117+
)
118+
self.assertEqual(w, [])
119+
76120

77121
class IndexSeriesConstructor(TestCase):
78122

0 commit comments

Comments
 (0)