Skip to content

Commit 42fe0f1

Browse files
authored
Fix read for scalar compound dataset (#761)
* Update parseDataset.m * Update writeCompound.m Support creating scalar/singleton dataspace for datasets with scalar size * Add flag to io.parseCompound for when dataset is scalar/singleton * Add test case * Fix failing tests
1 parent 541db1d commit 42fe0f1

File tree

7 files changed

+110
-6
lines changed

7 files changed

+110
-6
lines changed

+io/parseCompound.m

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
function data = parseCompound(datasetId, data)
1+
function data = parseCompound(datasetId, data, isScalar)
22
%did is the dataset_id for the containing dataset
33
%data should be a scalar struct with fields as columns
4+
if nargin < 3; isScalar = false; end
45
if isempty(data)
56
return;
67
end
@@ -10,6 +11,7 @@
1011
isReferenceType = false(1, numFields);
1112
isCharacterType = false(1, numFields);
1213
isLogicalType = false(1,numFields);
14+
isScalarCellStr = false(1,numFields);
1315
for iField = 1:numFields
1416
fieldTypeId = H5T.get_member_type(typeId, iField-1);
1517
subTypeId{iField} = fieldTypeId;
@@ -20,6 +22,7 @@
2022
%if not variable len (which would make it a cell array)
2123
%then mark for transpose
2224
isCharacterType(iField) = ~H5T.is_variable_str(fieldTypeId);
25+
isScalarCellStr(iField) = isScalar && ~isCharacterType(iField);
2326
case H5ML.get_constant_value('H5T_ENUM')
2427
isLogicalType(iField) = io.isBool(fieldTypeId);
2528
% Note: There is currently no postprocessing applied for
@@ -62,4 +65,11 @@
6265
name = logicalFieldName{iFieldName};
6366
data.(name) = io.internal.h5.postprocess.toLogical(data.(name));
6467
end
65-
end
68+
69+
% unpack scalar cellstr
70+
scalarCellstrFieldName = fieldName(isScalarCellStr);
71+
for iFieldName = 1:length(scalarCellstrFieldName)
72+
name = scalarCellstrFieldName{iFieldName};
73+
data.(name) = data.(name){1};
74+
end
75+
end

+io/parseDataset.m

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@
5151
name, length(datatype.Type.Member));
5252
data = io.internal.h5.postprocess.toEnumCellStr(data, datatype.Type);
5353
end
54+
case 'H5T_COMPOUND'
55+
isScalar = true;
56+
data = io.parseCompound(did, data, isScalar);
5457
end
5558
else
5659
sid = H5D.get_space(did);

+io/writeCompound.m

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ function writeCompound(fid, fullpath, data, varargin)
2727
% Example:
2828
% io.writeCompound(fid, '/group/dataset', data);
2929

30+
31+
forceArray = any(strcmp('forceArray', varargin));
32+
forceMatrix = any(strcmp('forceMatrix', varargin));
33+
3034
%convert to a struct
3135
if istable(data)
3236
data = table2struct(data);
@@ -129,7 +133,11 @@ function writeCompound(fid, fullpath, data, varargin)
129133
end
130134

131135
try
132-
sid = H5S.create_simple(1, numrows, []);
136+
if numrows == 1 && ~(forceArray || forceMatrix)
137+
sid = H5S.create('H5S_SCALAR');
138+
else
139+
sid = H5S.create_simple(1, numrows, []);
140+
end
133141
did = H5D.create(fid, fullpath, tid, sid, 'H5P_DEFAULT');
134142
catch ME
135143
if contains(ME.message, 'name already exists')
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
classdef ScalarCompoundTest < tests.abstract.NwbTestCase
2+
% ScalarCompoundTest - Test that a scalar compound dataset is imported correctly
3+
4+
methods (TestMethodSetup)
5+
function setupMethod(testCase)
6+
% Use a fixture to create a temporary working directory
7+
testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture);
8+
end
9+
end
10+
11+
methods (Test)
12+
function testScalarCompoundIO(testCase)
13+
14+
% Generate the compound test schema using fixture
15+
testCase.applyTestSchemaFixture('rrs');
16+
testCase.applyTestSchemaFixture('cs');
17+
18+
% Set up file with scalar compound dataset
19+
nwb = tests.factory.NWBFile();
20+
21+
ts = tests.factory.TimeSeriesWithTimestamps();
22+
nwb.acquisition.set('timeseries', ts);
23+
24+
% Create a structure matching the compound type definition.
25+
data = struct(...
26+
'integer', int32(0), ...
27+
'float', 0, ...
28+
'text', 'test', ...
29+
'boolean', false, ...
30+
'reference', types.untyped.ObjectView(ts));
31+
32+
% Create data type and add to nwb object
33+
scalarCompound = types.cs.ScalarCompoundMixedData('data', data);
34+
nwb.analysis.set('ScalarCompound', scalarCompound);
35+
36+
% Export
37+
fileName = testCase.getRandomFilename();
38+
nwbExport(nwb, fileName);
39+
40+
% Read
41+
nwbIn = nwbRead(fileName, 'ignorecache');
42+
scalarCompoundIn = nwbIn.analysis.get('ScalarCompound');
43+
44+
% Verify that dataset was stored as scalar/singleton dataset
45+
info = h5info(fileName, '/analysis/ScalarCompound/data');
46+
testCase.verifyEqual(info.Dataspace.Type, 'scalar', ...
47+
'Expected compound dataset to be saved as scalar/singleton dataset (H5S_SCALAR)')
48+
49+
% Verify that subtypes were properly postprocessed on read
50+
testCase.verifyClass(scalarCompoundIn.data.float, 'double')
51+
testCase.verifyClass(scalarCompoundIn.data.text, 'char')
52+
testCase.verifyClass(scalarCompoundIn.data.boolean, 'logical')
53+
testCase.verifyClass(scalarCompoundIn.data.reference, 'types.untyped.ObjectView')
54+
testCase.verifyEqual(scalarCompoundIn.data.text, 'test')
55+
end
56+
end
57+
end

+tests/+unit/+io/WriteTest.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ function testWriteCompoundOverWrite(testCase)
144144

145145
% Initial data to write (e.g., 10x10)
146146
initialData = struct('a', 1, 'b', true, 'c', 'test');
147-
io.writeCompound(fid, fullPath, initialData); % First write to create the dataset
147+
io.writeCompound(fid, fullPath, initialData, 'forceArray'); % First write to create the dataset
148148

149149
% Attempt to write data of a different size (e.g., 5x5)
150150
newData = cat(1, initialData, struct('a', 2, 'b', false, 'c', 'new test'));

+tests/test-schema/compoundSchema/cs.compoundtypes.yaml

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,30 @@ groups:
2424
target_type: RefContainer
2525
reftype: region
2626
shape:
27-
- null
27+
- null
28+
29+
- neurodata_type_def: ScalarCompoundMixedData
30+
neurodata_type_inc: NWBContainer
31+
doc: Data type with scalar compound data type
32+
datasets:
33+
- name: data
34+
shape:
35+
- 1
36+
dtype:
37+
- name: integer
38+
dtype: int
39+
doc: An integer value.
40+
- name: float
41+
dtype: float
42+
doc: A float value.
43+
- name: text
44+
dtype: text
45+
doc: A text value.
46+
- name: boolean
47+
dtype: bool
48+
doc: A bolean value.
49+
- name: reference
50+
doc: Reference to a NWBDataInterface
51+
dtype:
52+
target_type: NWBDataInterface
53+
reftype: object

+types/+untyped/MetaClass.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
refs = obj.data.export(fid, fullpath, refs);
2626
elseif istable(obj.data) || isstruct(obj.data) ||...
2727
isa(obj.data, 'containers.Map')
28-
io.writeCompound(fid, fullpath, obj.data);
28+
io.writeCompound(fid, fullpath, obj.data, 'forceArray');
2929
else
3030
io.writeDataset(fid, fullpath, obj.data, 'forceArray');
3131
end

0 commit comments

Comments
 (0)