Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Fix TimeSeries data does not match length of timestamps should raise an error when created #1538

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/gallery/domain/brain_observatory.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
data=dataset.get_stimulus_template(stimulus),
unit='NA',
format='raw',
timestamps=[0.0])
timestamps=timestamps[dataset.get_stimulus_table(stimulus).start.values])
image_index = IndexSeries(
name=stimulus,
data=dataset.get_stimulus_table(stimulus).frame.values,
Expand Down
20 changes: 13 additions & 7 deletions src/pynwb/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,28 +189,34 @@ def __init__(self, **kwargs):
else:
raise TypeError("either 'timestamps' or 'rate' must be specified")

if not self._check_time_series_dimension():
warn("%s '%s': Length of data does not match length of timestamps. Your data may be transposed. "
"Time should be on the 0th dimension" % (self.__class__.__name__, self.name))
self._error_on_new_warn_on_construct(
error_msg=self._check_time_series_dimension()
)

def _check_time_series_dimension(self):
"""Check that the 0th dimension of data equals the length of timestamps, when applicable.
"""
if self.timestamps is None:
return True
return

data_shape = get_data_shape(data=self.fields["data"], strict_no_data_load=True)
timestamps_shape = get_data_shape(data=self.fields["timestamps"], strict_no_data_load=True)

# skip check if shape of data or timestamps cannot be computed
if data_shape is None or timestamps_shape is None:
return True
return

# skip check if length of the first dimension is not known
if data_shape[0] is None or timestamps_shape[0] is None:
return True
return

return data_shape[0] == timestamps_shape[0]
if data_shape[0] == timestamps_shape[0]:
return

return (
"%s '%s': Length of data does not match length of timestamps. Your data may be transposed. "
"Time should be on the 0th dimension" % (self.__class__.__name__, self.name)
)

@property
def num_samples(self):
Expand Down
16 changes: 6 additions & 10 deletions src/pynwb/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,9 @@ def __init__(self, **kwargs):
DeprecationWarning,
)

if not self._check_image_series_dimension():
warnings.warn(
"%s '%s': Length of data does not match length of timestamps. Your data may be transposed. "
"Time should be on the 0th dimension"
% (self.__class__.__name__, self.name)
)

self._error_on_new_warn_on_construct(
error_msg=self._check_image_series_dimension()
)
self._error_on_new_warn_on_construct(
error_msg=self._check_external_file_starting_frame_length()
)
Expand All @@ -135,17 +131,17 @@ def _check_time_series_dimension(self):
"""Override _check_time_series_dimension to do nothing.
The _check_image_series_dimension method will be called instead.
"""
return True
return

def _check_image_series_dimension(self):
"""Check that the 0th dimension of data equals the length of timestamps, when applicable.

ImageSeries objects can have an external file instead of data stored. The external file cannot be
queried for the number of frames it contains, so this check will return True when an external file
queried for the number of frames it contains, so this check will return when an external file
is provided. Otherwise, this function calls the parent class' _check_time_series_dimension method.
"""
if self.external_file is not None:
return True
return
return super()._check_time_series_dimension()

def _check_external_file_starting_frame_length(self):
Expand Down
38 changes: 36 additions & 2 deletions tests/unit/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,19 +373,53 @@ def test_conflicting_time_args(self):
timestamps=[0.3, 0.4, 0.5],
)

def test_dimension_warning(self):
def test_timestamps_data_length_error_raised(self):
"""Test that TimeSeries cannot be created with timestamps and data of different lengths."""
msg = (
"TimeSeries 'test_ts2': Length of data does not match length of timestamps. Your data may be "
"transposed. Time should be on the 0th dimension"
)
with self.assertWarnsWith(UserWarning, msg):
with self.assertRaisesWith(ValueError, msg):
TimeSeries(
name="test_ts2",
data=[10, 11, 12],
unit="grams",
timestamps=[0.3, 0.4, 0.5, 0.6, 0.7, 0.8],
)

def test_timestamps_data_length_warning_construct_mode(self):
"""
Test that warning is raised when the length of data does not match the length of
timestamps in case that the TimeSeries in construct mode (i.e., during read).
"""
msg = (
"TimeSeries 'test_ts2': Length of data does not match length of timestamps. Your data may be "
"transposed. Time should be on the 0th dimension"
)
for timestamps in [[0], [1, 2, 3, 4]]:
with self.subTest():
# Create the time series in construct mode, modelling the behavior
# of the ObjectMapper on read while avoiding having to create, write,
# and read and entire NWB file
obj = TimeSeries.__new__(
TimeSeries,
container_source=None,
parent=None,
object_id="test",
in_construct_mode=True,
)
with self.assertWarnsWith(UserWarning, msg):
obj.__init__(
name="test_ts2",
data=[10, 11, 12],
unit="grams",
timestamps=timestamps,
)
# Disable construct mode. Since we are not using this object anymore
# this is not strictly necessary but is good style in case we expand
# the test later on.
obj._in_construct_mode = False


class TestImage(TestCase):
def test_init(self):
Expand Down
20 changes: 14 additions & 6 deletions tests/unit/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,13 @@ def test_external_file_no_unit(self):
)
self.assertEqual(iS.unit, ImageSeries.DEFAULT_UNIT)

def test_dimension_warning(self):
"""Test that a warning is raised when the dimensions of the data are not the
same as the dimensions of the timestamps."""
def test_dimension_error(self):
"""Test that ImageSeries cannot be created with timestamps and data of different lengths."""
msg = (
"ImageSeries 'test_iS': Length of data does not match length of timestamps. Your data may be "
"transposed. Time should be on the 0th dimension"
)
with self.assertWarnsWith(UserWarning, msg):
with self.assertRaisesWith(ValueError, msg):
ImageSeries(
name='test_iS',
data=np.ones((3, 3, 3)),
Expand All @@ -98,9 +97,14 @@ def test_dimension_warning(self):
)

def test_dimension_warning_external_file_with_timestamps(self):
"""Test that a warning is not raised when external file is used with timestamps."""
"""Test that warning is not raised when external file is used with timestamps."""
obj = ImageSeries.__new__(ImageSeries,
container_source=None,
parent=None,
object_id="test",
in_construct_mode=True)
with warnings.catch_warnings(record=True) as w:
ImageSeries(
obj.__init__(
name='test_iS',
external_file=['external_file'],
format='external',
Expand All @@ -109,6 +113,10 @@ def test_dimension_warning_external_file_with_timestamps(self):
timestamps=[1, 2, 3, 4]
)
self.assertEqual(w, [])
# Disable construct mode. Since we are not using this object any more
# this is not strictly necessary but is good style in case we expand
# the test later on
obj._in_construct_mode = False

def test_dimension_warning_external_file_with_rate(self):
"""Test that a warning is not raised when external file is used with rate."""
Expand Down