diff --git a/src/datajoint/blob.py b/src/datajoint/blob.py index 8651a57af..292350ad7 100644 --- a/src/datajoint/blob.py +++ b/src/datajoint/blob.py @@ -474,12 +474,30 @@ def pack_struct(self, array): ) # values def read_cell_array(self): - """deserialize MATLAB cell array""" + """ + Deserialize MATLAB cell array. + + Handles edge cases from MATLAB: + - Empty cell arrays ({}) + - Cell arrays with empty elements ({[], [], []}) + - Nested arrays ({[1,2], [3,4,5]}) - ragged arrays + - Cell matrices with mixed content + """ n_dims = self.read_value() shape = self.read_value(count=n_dims) n_elem = int(np.prod(shape)) result = [self.read_blob(n_bytes=self.read_value()) for _ in range(n_elem)] - return (self.squeeze(np.array(result).reshape(shape, order="F"), convert_to_scalar=False)).view(MatCell) + + # Handle empty cell array + if n_elem == 0: + return np.empty(0, dtype=object).view(MatCell) + + # Use object dtype to handle ragged/nested arrays without reshape errors. + # This avoids NumPy's array homogeneity requirements that cause failures + # with MATLAB cell arrays containing arrays of different sizes. + arr = np.empty(n_elem, dtype=object) + arr[:] = result + return self.squeeze(arr.reshape(shape, order="F"), convert_to_scalar=False).view(MatCell) def pack_cell_array(self, array): return ( diff --git a/tests/integration/test_blob_matlab.py b/tests/integration/test_blob_matlab.py index 630a9ac66..b7b05a0cb 100644 --- a/tests/integration/test_blob_matlab.py +++ b/tests/integration/test_blob_matlab.py @@ -160,3 +160,71 @@ def test_iter(schema_blob_pop): from_iter = {d["id"]: d for d in Blob()} assert len(from_iter) == len(Blob()) assert from_iter[1]["blob"] == "character string" + + +def test_cell_array_with_nested_arrays(): + """ + Test unpacking MATLAB cell arrays containing arrays of different sizes. + Regression test for issue #1098. + """ + # Create a cell array with nested arrays of different sizes (ragged) + cell = np.empty(2, dtype=object) + cell[0] = np.array([1, 2, 3]) + cell[1] = np.array([4, 5, 6, 7, 8]) + cell = cell.reshape((1, 2)).view(dj.MatCell) + + # Pack and unpack + packed = pack(cell) + unpacked = unpack(packed) + + # Should preserve structure + assert isinstance(unpacked, dj.MatCell) + assert unpacked.shape == (1, 2) + assert_array_equal(unpacked[0, 0], np.array([1, 2, 3])) + assert_array_equal(unpacked[0, 1], np.array([4, 5, 6, 7, 8])) + + +def test_cell_array_with_empty_elements(): + """ + Test unpacking MATLAB cell arrays containing empty arrays. + Regression test for issue #1056. + """ + # Create a cell array with empty elements: {[], [], []} + cell = np.empty(3, dtype=object) + cell[0] = np.array([]) + cell[1] = np.array([]) + cell[2] = np.array([]) + cell = cell.reshape((3, 1)).view(dj.MatCell) + + # Pack and unpack + packed = pack(cell) + unpacked = unpack(packed) + + # Should preserve structure + assert isinstance(unpacked, dj.MatCell) + assert unpacked.shape == (3, 1) + for i in range(3): + assert unpacked[i, 0].size == 0 + + +def test_cell_array_mixed_empty_nonempty(): + """ + Test unpacking MATLAB cell arrays with mixed empty and non-empty elements. + """ + # Create a cell array: {[1,2], [], [3,4,5]} + cell = np.empty(3, dtype=object) + cell[0] = np.array([1, 2]) + cell[1] = np.array([]) + cell[2] = np.array([3, 4, 5]) + cell = cell.reshape((3, 1)).view(dj.MatCell) + + # Pack and unpack + packed = pack(cell) + unpacked = unpack(packed) + + # Should preserve structure + assert isinstance(unpacked, dj.MatCell) + assert unpacked.shape == (3, 1) + assert_array_equal(unpacked[0, 0], np.array([1, 2])) + assert unpacked[1, 0].size == 0 + assert_array_equal(unpacked[2, 0], np.array([3, 4, 5]))