diff --git a/CHANGELOG.md b/CHANGELOG.md index 39213a72d..4832ab303 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## PyNWB 2.1.0 (Upcoming) ### Breaking changes: -- Restrict `SpatialSeries.data` to have no more than 3 columns (#1455) +- Raise a warning if `SpatialSeries.data` has more than 3 columns (#1455, #1480) - Updated ``TimeIntervals`` to use the new ``TimeSeriesReferenceVectorData`` type. This does not alter the overall structure of ``TimeIntervals`` in a major way aside from changing the value of the ``neurodata_type`` attribute of the ``TimeIntervals.timeseries`` column from ``VectorData`` to ``TimeSeriesReferenceVectorData``. This change facilitates diff --git a/src/pynwb/behavior.py b/src/pynwb/behavior.py index 8638d1f25..cd981c76f 100644 --- a/src/pynwb/behavior.py +++ b/src/pynwb/behavior.py @@ -1,4 +1,6 @@ -from hdmf.utils import docval, popargs, get_docval +import warnings + +from hdmf.utils import docval, popargs, get_docval, get_data_shape from . import register_class, CORE_NAMESPACE from .core import MultiContainerInterface @@ -21,9 +23,7 @@ class SpatialSeries(TimeSeries): __nwbfields__ = ('reference_frame',) @docval(*get_docval(TimeSeries.__init__, 'name'), # required - {'name': 'data', 'type': ('array_data', 'data', TimeSeries), 'shape': ( - (None, ), (None, 1), (None, 2), (None, 3) - ), # required + {'name': 'data', 'type': ('array_data', 'data', TimeSeries), 'shape': ((None, ), (None, None)), # required 'doc': ('The data values. Can be 1D or 2D. The first dimension must be time. If 2D, there can be 1, 2, ' 'or 3 columns, which represent x, y, and z.')}, {'name': 'reference_frame', 'type': str, # required @@ -38,8 +38,26 @@ def __init__(self, **kwargs): """ name, data, reference_frame, unit = popargs('name', 'data', 'reference_frame', 'unit', kwargs) super(SpatialSeries, self).__init__(name, data, unit, **kwargs) + + # NWB 2.5 restricts length of second dimension to be <= 3 + allowed_data_shapes = ((None, ), (None, 1), (None, 2), (None, 3)) + data_shape = get_data_shape(data) + if not any(self._validate_data_shape(data_shape, a) for a in allowed_data_shapes): + warnings.warn("SpatialSeries '%s' has data shape %s which is not compliant with NWB 2.5 and greater. " + "The second dimension should have length <= 3 to represent at most x, y, z." % + (name, str(data_shape))) + self.reference_frame = reference_frame + @staticmethod + def _validate_data_shape(valshape, argshape): + if not len(valshape) == len(argshape): + return False + for a, b in zip(valshape, argshape): + if b not in (a, None): + return False + return True + @register_class('BehavioralEpochs', CORE_NAMESPACE) class BehavioralEpochs(MultiContainerInterface): diff --git a/tests/unit/test_behavior.py b/tests/unit/test_behavior.py index 9ba75e128..664365070 100644 --- a/tests/unit/test_behavior.py +++ b/tests/unit/test_behavior.py @@ -20,15 +20,11 @@ def test_set_unit(self): self.assertEqual(sS.unit, 'degrees') def test_gt_3_cols(self): - with self.assertRaises(ValueError) as error: + msg = ("SpatialSeries 'test_sS' has data shape (5, 4) which is not compliant with NWB 2.5 and greater. " + "The second dimension should have length <= 3 to represent at most x, y, z.") + with self.assertWarnsWith(UserWarning, msg): SpatialSeries("test_sS", np.ones((5, 4)), "reference_frame", "meters", rate=30.) - self.assertEqual( - "SpatialSeries.__init__: incorrect shape for 'data' (got '(5, 4)', expected " - "'((None,), (None, 1), (None, 2), (None, 3))')", - str(error.exception) - ) - class BehavioralEpochsConstructor(TestCase): def test_init(self):