Skip to content

Commit 8d5173c

Browse files
ehennestadCopilot
andauthored
Fix: Ensure DataStub size is updated when the underlying dataset is expanded via a DataPipe (#766)
* Add getSize function for dataspace dimension retrieval Introduces io.space.getSize to obtain current and maximum dataset dimensions from an HDF5 dataspace identifier. Handles dimension order conversion and unlimited dimension representation for MATLAB compatibility. * Update DataStub.m - Changed get_space to private method. - Add maxDims property - Added updateSize method, which can be accessed by BoundPipe, as stubs of BoundPipes are expandable and their size can change - Improved subsref override - Support method calls with no output - Support compound indexing * Update dataStubTest.m Add test for nested data indexing * Update BoundPipe.m - Update DataStub size when dataset is appended/expanded - Delegate detection of current size and max size to datastub * Simplify DataStub/export * Fix typo: Datastub -> DataStub * Add boolean type to compound dataset for improved testing * Update ExtensionGenerationFixture.m Improve fixture teardown * Update dataStubTest.m * Refactor dims and maxDims getters to use updateSize Simplifies the logic in the dims and maxDims property getters by calling updateSize instead of duplicating code. Also updates the access level for updateSize to allow access from DataStub and BoundPipe classes. * Update +types/+untyped/@DataStub/export.m Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 42fe0f1 commit 8d5173c

File tree

5 files changed

+134
-39
lines changed

5 files changed

+134
-39
lines changed

+io/+space/getSize.m

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
function [datasetSize, datasetMaxSize] = getSize(spaceId)
2+
% getSize - Retrieves the current and maximum sizes of a dataset.
3+
%
4+
% Syntax:
5+
% [datasetSize, datasetMaxSize] = io.space.getSize(spaceId)
6+
%
7+
% Input Arguments:
8+
% spaceId {H5ML.id} - Identifier for the dataspace from which
9+
% the dimensions are retrieved.
10+
%
11+
% Output Arguments:
12+
% datasetSize - Current size of the dataset dimensions.
13+
% datasetMaxSize - Maximum size of the dataset dimensions.
14+
%
15+
% Note:
16+
% - Flips dimensions as the h5 function returns dimensions in C-style order
17+
% whereas MATLAB represents data in F-style order
18+
% - Replaces H5 constants with Inf for unlimited dimensions
19+
20+
arguments
21+
spaceId {matnwb.common.compatibility.mustBeA(spaceId, "H5ML.id")}
22+
end
23+
24+
[~, h5Dims, h5MaxDims] = H5S.get_simple_extent_dims(spaceId);
25+
datasetSize = fliplr(h5Dims);
26+
datasetMaxSize = fliplr(h5MaxDims);
27+
28+
h5Unlimited = H5ML.get_constant_value('H5S_UNLIMITED');
29+
datasetMaxSize(datasetMaxSize == h5Unlimited) = Inf;
30+
end

+tests/+fixtures/ExtensionGenerationFixture.m

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,13 @@ function clearGenerated(fixture)
4646
namespaceName = extractBefore(namespaceFilename, '.');
4747

4848
generatedTypesDirectory = fullfile(fixture.TypesOutputFolder, "+types", "+"+namespaceName);
49-
rmdir(generatedTypesDirectory, 's');
50-
49+
if isfolder(generatedTypesDirectory)
50+
rmdir(generatedTypesDirectory, 's');
51+
end
5152
cacheFile = fullfile(fixture.TypesOutputFolder, "namespaces", namespaceName+".mat");
52-
delete(cacheFile)
53+
if isfile(cacheFile)
54+
delete(cacheFile)
55+
end
5356
end
5457
end
5558
end

+tests/+unit/dataStubTest.m

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
classdef dataStubTest < tests.abstract.NwbTestCase
22

3+
methods (TestClassSetup)
4+
function generateTestSchemas(testCase)
5+
% Generate the rrs and cs test extensions for use in all tests
6+
% of this test suite, using fixture for proper cleanup
7+
testCase.applyTestSchemaFixture('rrs');
8+
testCase.applyTestSchemaFixture('cs');
9+
end
10+
end
11+
312
methods (TestMethodSetup)
413
function setupMethod(testCase)
514
% Use a fixture to create a temporary working directory
@@ -70,9 +79,6 @@ function testRegionRead(testCase)
7079

7180
function testObjectCopy(testCase)
7281

73-
testCase.applyTestSchemaFixture('rrs');
74-
testCase.applyTestSchemaFixture('cs');
75-
7682
nwb = NwbFile(...
7783
'identifier', 'DATASTUB',...
7884
'session_description', 'test datastub object copy',...
@@ -91,10 +97,12 @@ function testObjectCopy(testCase)
9197
nwb.acquisition.set('rc', rc);
9298
nwb.analysis.set('rcRef', rcRef);
9399
nwbExport(nwb, 'original.nwb');
100+
94101
nwbOriginalIn = nwbRead('original.nwb', 'ignorecache');
95102
tests.util.verifyContainerEqual(testCase, nwbOriginalIn, nwb);
96103

97104
nwbExport(nwbOriginalIn, 'copy.nwb');
105+
98106
nwbCopyIn = nwbRead('copy.nwb', 'ignorecache');
99107
tests.util.verifyContainerEqual(testCase, nwbCopyIn, nwb);
100108
end
@@ -138,11 +146,6 @@ function testLoadWithEmptyIndices(testCase)
138146
end
139147

140148
function testResolveCompoundDataType(testCase)
141-
142-
% Generate the compound test schema using fixture
143-
testCase.applyTestSchemaFixture('rrs');
144-
testCase.applyTestSchemaFixture('cs');
145-
146149
% Set up file with compound dataset
147150
nwb = tests.factory.NWBFile();
148151

@@ -178,5 +181,35 @@ function testResolveCompoundDataType(testCase)
178181
compoundRefInDirectRead.dims, ...
179182
compoundRefIn.data.dims )
180183
end
184+
185+
function testNestedDataIndexing(testCase)
186+
% Set up file with compound dataset
187+
188+
nwb = tests.factory.NWBFile();
189+
190+
ts = tests.factory.TimeSeriesWithTimestamps();
191+
nwb.acquisition.set('timeseries', ts);
192+
193+
tsPath = '/acquisition/timeseries';
194+
tsDataPath = [tsPath '/data'];
195+
196+
compoundRef = types.cs.CompoundRefData('data', table(...
197+
rand(2, 1),...
198+
rand(2, 1),...
199+
[true; false],...
200+
[types.untyped.ObjectView(tsPath); types.untyped.ObjectView(tsPath)],...
201+
[types.untyped.RegionView(tsDataPath, 1:2); types.untyped.RegionView(tsDataPath, 2:3)],...
202+
'VariableNames', {'a', 'b', 'c', 'objref', 'regref'}));
203+
204+
nwb.analysis.set('compoundRef', compoundRef);
205+
nwbExport(nwb, 'test.nwb');
206+
207+
% Read in data
208+
nwbIn = nwbRead('test.nwb', 'ignorecache');
209+
compoundRefIn = nwbIn.analysis.get('compoundRef');
210+
211+
testCase.verifyClass(compoundRefIn.data(1:2).a, 'double');
212+
testCase.verifyLength(compoundRefIn.data(1:2).a, 2);
213+
end
181214
end
182215
end

+types/+untyped/+datapipe/BoundPipe.m

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
properties (SetAccess = private)
55
config = types.untyped.datapipe.Configuration.empty;
66
pipeProperties = {};
7-
stub = types.untyped.DataStub.empty;
7+
stub types.untyped.DataStub = types.untyped.DataStub.empty;
88
end
99

1010
properties (SetAccess = private, Dependent)
@@ -24,14 +24,8 @@
2424

2525
obj.stub = types.untyped.DataStub(filename, path);
2626

27-
sid = obj.stub.get_space();
28-
[~, h5_dims, h5_maxdims] = H5S.get_simple_extent_dims(sid);
29-
H5S.close(sid);
30-
31-
current_size = fliplr(h5_dims);
32-
max_size = fliplr(h5_maxdims);
33-
h5_unlimited = H5ML.get_constant_value('H5S_UNLIMITED');
34-
max_size(max_size == h5_unlimited) = Inf;
27+
current_size = obj.stub.dims;
28+
max_size = obj.stub.maxDims;
3529

3630
did = obj.getDataset();
3731

@@ -224,6 +218,7 @@ function append(obj, data)
224218
H5F.close(fid);
225219

226220
obj.config.offset = obj.config.offset + data_size(obj.config.axis);
221+
obj.stub.updateSize()
227222
end
228223

229224
function property = getPipeProperty(obj, type)

+types/+untyped/@DataStub/DataStub.m

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,15 @@
1313
ndims;
1414
dataType;
1515
end
16+
17+
properties (Dependent, SetAccess = private, GetAccess = ?types.untyped.datapipe.BoundPipe)
18+
maxDims
19+
end
20+
1621
properties (Access = private)
1722
dims_ double
18-
dataType_ {mustBeA(dataType_, ["char", "string", "struct"])} = string.empty % Can be char (simple type) or struct (compound type descriptor)
23+
dataType_ {matnwb.common.compatibility.mustBeA(dataType_, ["char", "string", "struct"])} = string.empty % Can be char (simple type) or struct (compound type descriptor)
24+
maxDims_ double
1925
end
2026

2127
methods
@@ -37,25 +43,21 @@
3743
obj.dataType_ = dataType; % Keep as struct for compound types
3844
end
3945
end
40-
41-
function sid = get_space(obj) % Todo: private method
42-
fid = H5F.open(obj.filename);
43-
did = H5D.open(fid, obj.path);
44-
sid = H5D.get_space(did);
45-
H5D.close(did);
46-
H5F.close(fid);
47-
end
48-
46+
4947
function dims = get.dims(obj)
5048
if isempty(obj.dims_)
51-
sid = obj.get_space();
52-
[~, h5_dims, ~] = H5S.get_simple_extent_dims(sid);
53-
obj.dims_ = fliplr(h5_dims);
54-
H5S.close(sid);
49+
obj.updateSize()
5550
end
5651
dims = obj.dims_;
5752
end
58-
53+
54+
function maxDims = get.maxDims(obj)
55+
if isempty(obj.maxDims_)
56+
obj.updateSize()
57+
end
58+
maxDims = obj.maxDims_;
59+
end
60+
5961
function nd = get.ndims(obj)
6062
nd = length(obj.dims);
6163
end
@@ -185,10 +187,10 @@
185187

186188
refs = export(obj, fid, fullpath, refs);
187189

188-
function B = subsref(obj, S)
190+
function varargout = subsref(obj, S)
189191
CurrentSubRef = S(1);
190192
if ~isscalar(obj) || strcmp(CurrentSubRef.type, '.')
191-
B = builtin('subsref', obj, S);
193+
[varargout{1:nargout}] = builtin('subsref', obj, S);
192194
return;
193195
end
194196

@@ -200,9 +202,9 @@
200202
selectionRank, rank);
201203
data = obj.load_mat_style(CurrentSubRef.subs{:});
202204
if isscalar(S)
203-
B = data;
205+
varargout = {data};
204206
else
205-
B = subsref(data, S(2:end));
207+
[varargout{1:nargout}] = subsref(data, S(2:end));
206208
end
207209
end
208210

@@ -225,4 +227,36 @@
225227
tf = isstruct(dt);
226228
end
227229
end
230+
231+
methods % Custom indexing
232+
function n = numArgumentsFromSubscript(obj, subs, indexingContext)
233+
if ~isscalar(subs) && strcmp(subs(1).type, '()')
234+
% Typical indexing pattern into compound data type, i.e
235+
% data(1:3).fieldName. Assume/expect one output.
236+
n = 1;
237+
else
238+
n = builtin('numArgumentsFromSubscript', obj, subs, indexingContext);
239+
end
240+
end
241+
end
242+
243+
methods (Access = {?types.untyped.DataStub, ?types.untyped.datapipe.BoundPipe})
244+
function updateSize(obj)
245+
% updateSize - Should be called to initialize values or when dataset
246+
% space is expanded
247+
sid = get_space(obj);
248+
[obj.dims_, obj.maxDims_] = io.space.getSize(sid);
249+
H5S.close(sid);
250+
end
251+
end
252+
253+
methods (Access = private)
254+
function sid = get_space(obj)
255+
fid = H5F.open(obj.filename);
256+
did = H5D.open(fid, obj.path);
257+
sid = H5D.get_space(did);
258+
H5D.close(did);
259+
H5F.close(fid);
260+
end
261+
end
228262
end

0 commit comments

Comments
 (0)