Skip to content

Commit ba9f0c5

Browse files
authored
Fix dataset.data validation (#742)
* Fix dataset/data validation * Reset default assumption to scalar * Fix expansion of inherited keys for dataset classes * Regenerate type classes * Updated images tutorial Standardised variable names Added more comments about the image data permutation Added data types created in the last section to the nwb file Permuted the rgb_street data for valid nwb shape * Updated text sections of tutorial * minor change * Updated images tutorial Minor changes * Minor changes * Fix some failling tests * Update mss.multishapes.yaml Reordered keys to follow recommended key order Added shape constraint for NarrowInheritedDataset * Update MultipleShapesTest.m Added test to verify that shapes are correctly validated for inherited datasets of 'class_type' dataset * Add adhoc fix for inconsistent schema shape specifications * Additional fix for inconsistent schema specs * Change unconstrained shape from any to null * Cleanup and added comments
1 parent 67fa46e commit ba9f0c5

29 files changed

+170
-58
lines changed

+file/Dataset.m

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,14 @@
150150
);
151151

152152
if ~isempty(obj.dtype)
153-
props('data') = obj.dtype;
153+
% Add a value to the props map representing the dataset
154+
% itself. The prop name of a dataset class is always data,
155+
% the type should be empty, and we add a custom doc.
156+
objCopy = obj;
157+
objCopy.name = 'data';
158+
objCopy.type = ''; % Reset type, as this now represents a property
159+
objCopy.doc = sprintf('Data property for dataset class (%s)', obj.type);
160+
props('data') = objCopy;
154161
end
155162

156163
if ~isempty(obj.attributes)

+file/fillValidators.m

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,10 @@
187187

188188
function fdvstr = fillDimensionValidation(name, shape)
189189

190+
if isnumeric(shape) && isnan(shape) % Any shape is allowed
191+
fdvstr = ''; return
192+
end
193+
190194
if iscell(shape)
191195
if ~isempty(shape) && iscell(shape{1})
192196
for i = 1:length(shape)

+file/formatShape.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
function sz = formatShape(shape)
22
%check for optional dims
3+
4+
if ~iscell(shape) && isnumeric(shape) && isnan(shape)
5+
sz = shape; return
6+
end
7+
38
assert(iscell(shape), '`shape` must be a cell.');
49

510
if isempty(shape)

+file/isShapeScalar.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
function isScalar = isShapeScalar(shape)
2+
if isnumeric(shape) && isnan(shape); isScalar = false; return; end
23
if ~iscell(shape)
34
shape = {shape};
45
elseif iscell(shape{1})
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
function classSpec = applySchemaVersionPatches(className, classSpec, propSpecs, namespaceInfo)
2+
% applySchemaVersionPatches - Applies patches to schema specifications for classes.
3+
4+
switch className
5+
case 'ScratchData'
6+
% Spec does not define a shape, so it defaults to scalar, but
7+
% scratch data can be non-scalar
8+
fixShapeForDataProp(propSpecs)
9+
10+
case {'VectorData', 'VectorIndex'} % NWB v2.0.2 - v2.2.1
11+
% Spec for older NWB versions did not define a shape, so it
12+
% defaults to scalar, but these types support non-scalar data
13+
if strcmp(namespaceInfo.name, 'core') && ...
14+
ismember(namespaceInfo.version, {'2.0.2', '2.1.0'})
15+
fixShapeForDataProp(propSpecs)
16+
classSpec = fixShapeForDataClass(classSpec);
17+
elseif strcmp(namespaceInfo.name, 'hdmf_common') && ...
18+
ismember(namespaceInfo.version, {'1.1.0', '1.1.2'})
19+
fixShapeForDataProp(propSpecs)
20+
classSpec = fixShapeForDataClass(classSpec);
21+
end
22+
end
23+
end
24+
25+
function fixShapeForDataProp(propSpecs)
26+
dataProp = propSpecs('data');
27+
dataProp.shape = nan;
28+
dataProp.scalar = false;
29+
propSpecs('data') = dataProp; %#ok<NASGU> Map is a handle object
30+
end
31+
function classSpec = fixShapeForDataClass(classSpec)
32+
classSpec.shape = nan;
33+
classSpec.scalar = false;
34+
end

+file/processClass.m

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
nodename = node(TYPEDEF_KEYS{hasTypeDefs});
1616

1717
if ~isKey(pregen, nodename)
18-
18+
1919
spec.internal.resolveInheritedFields(node, branch(iAncestor+1:end))
2020

2121
switch node('class_type')
@@ -32,6 +32,9 @@
3232
end
3333
props = class.getProps();
3434

35+
% Apply patches for special cases of schema/specification errors
36+
class = applySchemaVersionPatches(nodename, class, props, namespace);
37+
3538
pregen(nodename) = struct('class', class, 'props', props);
3639
end
3740
try
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
function expandInheritedFields(childSpec, ancestorSpec)
2+
% expandInheritedFields - Include specification keys from an ancestor
3+
% specification for keys that are missing in a child specification.
4+
5+
primitiveTypes = enumeration('spec.enum.Primitives');
6+
primitiveTypeKeys = {primitiveTypes.Key};
7+
8+
ignoreKeys = { ...
9+
'neurodata_type_def', ...
10+
'data_type_def', ...
11+
'neurodata_type_inc', ...
12+
'data_type_inc'};
13+
14+
ancestorSpecKeys = ancestorSpec.keys();
15+
for iKey = 1:numel(ancestorSpecKeys)
16+
currentAncestorKey = ancestorSpecKeys{iKey};
17+
if any(strcmp(currentAncestorKey, primitiveTypeKeys))
18+
if isKey(childSpec, currentAncestorKey)
19+
% Update specifications for nested HDMF primitive types
20+
spec.internal.updateSpecFromAncestorSpec(...
21+
childSpec(currentAncestorKey), ...
22+
ancestorSpec(currentAncestorKey))
23+
end
24+
elseif any(strcmp(currentAncestorKey, ignoreKeys))
25+
continue % skip ignore keys
26+
elseif ~isKey(childSpec, currentAncestorKey)
27+
childSpec(currentAncestorKey) = ancestorSpec(currentAncestorKey);
28+
end
29+
end
30+
end

+spec/+internal/resolveInheritedFields.m

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,15 @@ function resolveInheritedFields(typeSpec, ancestorTypeSpecs)
1515
for i = 1:length(ancestorTypeSpecs)
1616
ancestorType = ancestorTypeSpecs{i};
1717

18-
for j = 1:numel(primitiveTypes)
19-
primitiveKey = primitiveTypes(j).Key; % i.e: 'groups', 'datasets' etc.
20-
if isKey(typeSpec, primitiveKey) && isKey(ancestorType, primitiveKey)
21-
spec.internal.updateSpecFromAncestorSpec(...
22-
typeSpec(primitiveKey), ancestorType(primitiveKey))
18+
if strcmp(typeSpec('class_type'), 'datasets')
19+
spec.internal.expandInheritedFields(typeSpec, ancestorType)
20+
else % (class_type: group)
21+
for j = 1:numel(primitiveTypes)
22+
primitiveKey = primitiveTypes(j).Key; % i.e: 'groups', 'datasets' etc.
23+
if isKey(typeSpec, primitiveKey) && isKey(ancestorType, primitiveKey)
24+
spec.internal.updateSpecFromAncestorSpec(...
25+
typeSpec(primitiveKey), ancestorType(primitiveKey))
26+
end
2327
end
2428
end
2529
end

+spec/+internal/updateSpecFromAncestorSpec.m

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ function updateSpecFromAncestorSpec(childSpecs, ancestorSpecs)
1111
% Output Arguments:
1212
% None. The function modifies child specifications in place.
1313

14-
primitiveTypes = enumeration('spec.enum.Primitives');
15-
primitiveTypeKeys = {primitiveTypes.Key};
16-
1714
for iSpec = 1:numel(childSpecs)
1815
currentSpec = childSpecs{iSpec};
1916

@@ -24,19 +21,6 @@ function updateSpecFromAncestorSpec(childSpecs, ancestorSpecs)
2421
continue
2522
end
2623

27-
ancestorSpecKeys = matchingAncestorSpec.keys();
28-
for iKey = 1:numel(ancestorSpecKeys)
29-
currentAncestorKey = ancestorSpecKeys{iKey};
30-
if any(strcmp(currentAncestorKey, primitiveTypeKeys))
31-
if isKey(currentSpec, currentAncestorKey)
32-
% Recursively update nested specification primitives
33-
spec.internal.updateSpecFromAncestorSpec(...
34-
currentSpec(currentAncestorKey), ...
35-
matchingAncestorSpec(currentAncestorKey))
36-
end
37-
elseif ~isKey(currentSpec, currentAncestorKey)
38-
currentSpec(currentAncestorKey) = matchingAncestorSpec(currentAncestorKey);
39-
end
40-
end
24+
spec.internal.expandInheritedFields(currentSpec, matchingAncestorSpec)
4125
end
4226
end

+tests/+unit/+schema/MultipleShapesTest.m

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
methods (Test)
99
function testMultipleShapesDataset(testCase)
1010
msd = types.mss.MultiShapeDataset('data', rand(3, 1));
11-
msd.data = rand(1, 5, 7);
11+
msd.data = rand(7, 5, 1); % NB: Reverse dimensions relative to schema due to MATLAB F-ordering
1212
testCase.roundabout(msd);
1313
end
1414

@@ -17,7 +17,7 @@ function testNullShapeDataset(testCase)
1717
randiMax = intmax('int8') - 1;
1818
for i=1:100
1919
%test validation
20-
nsd.data = rand(randi(randiMax) + 1, 3);
20+
nsd.data = rand(3, randi(randiMax) + 1); % NB: Reverse dimensions relative to schema due to MATLAB F-ordering
2121
end
2222
testCase.roundabout(nsd);
2323
end
@@ -40,6 +40,17 @@ function testInheritedDtypeDataset(testCase)
4040
nid.data = 'Inherited Dtype Dataset';
4141
testCase.roundabout(nid);
4242
end
43+
44+
function testInheritedDatasetWithWrongShape(testCase)
45+
46+
% Should not fail for parent type that accepts data with shape 3
47+
types.mss.NullShapeDataset('data', {'A'; 'B'; 'C'});
48+
49+
% Should fail for inherited type that accepts data with shape 1
50+
testCase.verifyError(...
51+
@() types.mss.NarrowInheritedDataset('data', {'A'; 'B'; 'C'}'), ...
52+
'NWB:CheckDims:InvalidDimensions');
53+
end
4354
end
4455

4556
methods (Access = private)

0 commit comments

Comments
 (0)