Skip to content

Commit baa3aec

Browse files
committed
Merge branch 'main' into improve-dynamic-table-validation
2 parents 4457153 + 19239e8 commit baa3aec

20 files changed

Lines changed: 456 additions & 185 deletions

+file/mapType.m

Lines changed: 61 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,67 @@
11
function matlabType = mapType(dtype)
2-
%MAPTYPE
3-
% converts dtype name to type name. If struct, then returns a struct of mapped types
4-
% all this does is narrow the possible range of names per type.
2+
% mapType - Map an HDMF/NWB dtype descriptor to a MATLAB type descriptor
3+
%
4+
% This function normalizes the value of a schema ``dtype`` field into the
5+
% MATLAB type representation used by MatNWB code generation.
6+
%
7+
% Input:
8+
% dtype - Schema dtype descriptor. Supported forms are:
9+
% * character vector describing basic dtype, e.g. 'int', 'float32', 'text'
10+
% * cell array describing a compound dtype
11+
% * containers.Map describing a reference dtype
12+
%
13+
% Output:
14+
% matlabType - MATLAB type descriptor corresponding to ``dtype``:
15+
% * character vector for basic dtypes, e.g. 'int32', 'single', 'char'
16+
% * struct for compound dtypes, with one field per compound member
17+
% * containers.Map for reference dtypes
18+
%
19+
% Special cases:
20+
% * empty, 'None', and 'any' map to 'any'
21+
%
22+
% Raises an error if ``dtype`` is not a supported schema dtype.
523

6-
if isempty(dtype) || (ischar(dtype) && any(strcmpi({'None', 'any'}, dtype)))
7-
matlabType = 'any';
8-
return;
9-
end
10-
11-
if iscell(dtype)
12-
%compound type
13-
matlabType = struct();
14-
numTypes = length(dtype);
15-
for i=1:numTypes
16-
typeMap = dtype{i};
17-
typeName = typeMap('name');
18-
type = file.mapType(typeMap('dtype'));
19-
matlabType.(typeName) = type;
24+
persistent basicTypeMap
25+
if isempty(basicTypeMap)
26+
basicTypeMap = spec.getBasicDTypeMap();
2027
end
21-
return;
22-
end
2328

24-
if isa(dtype, 'containers.Map')
25-
matlabType = dtype;
26-
return;
27-
end
29+
if ischar(dtype) % Basic dtype
30+
if isempty(dtype) || any(strcmpi({'None', 'any'}, dtype))
31+
matlabType = 'any';
32+
else
33+
try
34+
matlabType = basicTypeMap(dtype);
35+
matlabType = char(matlabType);
36+
catch
37+
error('NWB:MapType:UnsupportedDtype', ...
38+
['Schema attribute `dtype` returned an unsupported value "%s". ' ...
39+
'If this value is a supported dtype according to the HDMF/NWB ' ...
40+
'specification language, please raise an issue on the MatNWB ' ...
41+
'GitHub repository'], dtype)
42+
end
43+
end
2844

29-
assert(ischar(dtype), 'NWB:MapType:InvalidDtype', ...
30-
'schema attribute `dtype` returned in unsupported type `%s`', class(dtype));
31-
32-
switch dtype
33-
case {'text', 'utf', 'utf8', 'utf-8', 'ascii', 'bytes'}
34-
matlabType = 'char';
35-
case 'bool'
36-
matlabType = 'logical';
37-
case 'isodatetime'
38-
matlabType = 'datetime';
39-
case {'float', 'float32'}
40-
matlabType = 'single';
41-
case 'float64'
42-
matlabType = 'double';
43-
case 'long'
44-
matlabType = 'int64';
45-
case 'int'
46-
matlabType = 'int8';
47-
case 'short'
48-
matlabType = 'int16';
49-
otherwise
45+
elseif iscell(dtype) % Compound dtype
46+
matlabType = struct();
47+
numTypes = numel(dtype);
48+
for i = 1:numTypes
49+
typeMap = dtype{i};
50+
typeName = typeMap('name');
51+
type = file.mapType(typeMap('dtype'));
52+
matlabType.(typeName) = type;
53+
end
54+
55+
elseif isa(dtype, 'containers.Map') % Reference dtype
5056
matlabType = dtype;
51-
end
57+
58+
elseif isempty(dtype)
59+
matlabType = 'any';
60+
61+
else
62+
error(...
63+
'NWB:MapType:InvalidDtype', ...
64+
['Schema `dtype` specification key must be a character vector, ', ...
65+
'cell array, or containers.Map; got %s.'], class(dtype))
66+
end
67+
end

+spec/getBasicDTypeMap.m

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
function mapping = getBasicDTypeMap()
2+
% getBasicDTypeMap - Map basic HDMF dtypes onto MATLAB types
3+
%
4+
% Reference:
5+
% https://github.com/hdmf-dev/hdmf/blob/5.1.0/src/hdmf/spec/spec.py#L31-L48
6+
7+
try
8+
mapping = dictionary();
9+
catch % Fallback for older MATLAB releases
10+
mapping = containers.Map('UniformValues', true);
11+
end
12+
13+
% Single precision floating point, 32 bit
14+
mapping('float') = 'single';
15+
mapping('float32') = 'single';
16+
17+
% Double precision floating point, 64 bit
18+
mapping('double') = 'double';
19+
mapping('float64') = 'double';
20+
21+
% Signed 64 bit integer, 64 bit
22+
mapping('long') = 'int64';
23+
mapping('int64') = 'int64';
24+
25+
% Signed 32 bit integer, 32 bit
26+
mapping('int32') = 'int32';
27+
28+
% Signed 16 bit integer, 16 bit
29+
mapping('short') = 'int16';
30+
mapping('int16') = 'int16';
31+
32+
% Signed 8 bit integer, 8 bit
33+
mapping('int') = 'int8';
34+
mapping('int8') = 'int8';
35+
36+
% Unsigned 64 bit integer, 64 bit
37+
mapping('uint64') = 'uint64';
38+
39+
% Unsigned 32 bit integer, 32 bit
40+
mapping('uint32') = 'uint32';
41+
42+
% Unsigned 16 bit integer, 16 bit
43+
mapping('uint16') = 'uint16';
44+
45+
% Unsigned 8 bit integer, 8 bit
46+
mapping('uint') = 'uint8';
47+
mapping('uint8') = 'uint8';
48+
49+
% Any numeric type (i.e., any int, uint, float), 8 to 64 bit
50+
mapping('numeric') = 'numeric';
51+
52+
% 8-bit Unicode, variable (UTF-8 encoding)
53+
mapping('text') = 'char';
54+
mapping('utf') = 'char';
55+
mapping('utf8') = 'char';
56+
mapping('utf-8') = 'char';
57+
58+
% ASCII text, variable (ASCII encoding)
59+
mapping('ascii') = 'char';
60+
mapping('bytes') = 'char';
61+
62+
% 8 bit integer with valid values 0 or 1, 8 bit
63+
mapping('bool') = 'logical';
64+
65+
% ISO 8601 datetime string, variable (ASCII encoding)
66+
mapping('isodatetime') = 'datetime';
67+
mapping('datetime') = 'datetime';
68+
end

+tests/+unit/+file/MapTypeTest.m

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
classdef MapTypeTest < matlab.unittest.TestCase
2+
3+
methods (Test)
4+
5+
function testMapBasicDtypes(testCase)
6+
testCase.verifyEqual(file.mapType('int'), 'int8')
7+
testCase.verifyEqual(file.mapType('uint'), 'uint8')
8+
testCase.verifyEqual(file.mapType('int32'), 'int32')
9+
testCase.verifyEqual(file.mapType('uint32'), 'uint32')
10+
testCase.verifyEqual(file.mapType('float32'), 'single')
11+
testCase.verifyEqual(file.mapType('text'), 'char')
12+
testCase.verifyEqual(file.mapType('bool'), 'logical')
13+
testCase.verifyEqual(file.mapType('isodatetime'), 'datetime')
14+
end
15+
16+
function testMapAnyDtypes(testCase)
17+
testCase.verifyEqual(file.mapType(''), 'any')
18+
testCase.verifyEqual(file.mapType([]), 'any')
19+
testCase.verifyEqual(file.mapType('None'), 'any')
20+
testCase.verifyEqual(file.mapType('any'), 'any')
21+
end
22+
23+
function testMapCompoundDtype(testCase)
24+
dtype = { ...
25+
containers.Map({'name', 'dtype'}, {'count', 'uint'}), ...
26+
containers.Map({'name', 'dtype'}, {'label', 'text'}) ...
27+
};
28+
29+
matlabType = file.mapType(dtype);
30+
31+
expectedType = struct('count', 'uint8', 'label', 'char');
32+
testCase.verifyEqual(matlabType, expectedType)
33+
end
34+
35+
function testMapReferenceDtypeReturnsMap(testCase)
36+
dtype = containers.Map( ...
37+
{'target_type', 'reftype'}, ...
38+
{'TimeSeries', 'object'});
39+
40+
matlabType = file.mapType(dtype);
41+
42+
testCase.verifyClass(matlabType, 'containers.Map')
43+
testCase.verifyEqual(matlabType, dtype)
44+
end
45+
46+
function testUnsupportedDtypeThrowsError(testCase)
47+
testCase.verifyError( ...
48+
@() file.mapType('unsupported_dtype'), ...
49+
'NWB:MapType:UnsupportedDtype')
50+
end
51+
52+
function testInvalidDtypeSpecificationThrowsError(testCase)
53+
testCase.verifyError( ...
54+
@() file.mapType(1), ...
55+
'NWB:MapType:InvalidDtype')
56+
end
57+
end
58+
end

+tests/+unit/dynamicTableTest.m

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,39 @@ function testGetNegativeId(testCase)
5555
end
5656
end
5757

58+
function testGetRowRequiresIdOnlyWhenUseIdIsTrue(testCase)
59+
dynamicTable = types.hdmf_common.DynamicTable( ...
60+
'description', 'test table', ...
61+
'colnames', {'vec'}, ...
62+
'vec', types.hdmf_common.VectorData( ...
63+
'description', 'data column', ...
64+
'data', (1:3)') ...
65+
);
66+
67+
dynamicTable.id = [];
68+
69+
testCase.verifyWarningFree(@() dynamicTable.getRow(1));
70+
testCase.verifyError( ...
71+
@() dynamicTable.getRow(1, 'useId', true), ...
72+
'NWB:DynamicTable:GetRow:MissingId');
73+
end
74+
75+
function testGetRowThrowsSpecificErrorForOutOfBoundsIndex(testCase)
76+
dynamicTable = util.table2nwb(table([1; 2], ["a"; "b"]));
77+
78+
testCase.verifyError( ...
79+
@() dynamicTable.getRow(3), ...
80+
'NWB:DynamicTable:GetRow:RowOutOfBounds');
81+
end
82+
83+
function testGetRowThrowsSpecificErrorForEmptyTable(testCase)
84+
dynamicTable = types.hdmf_common.DynamicTable();
85+
86+
testCase.verifyError( ...
87+
@() dynamicTable.getRow(1), ...
88+
'NWB:DynamicTable:GetRow:RowOutOfBounds');
89+
end
90+
5891
function testNwbToTableWithReferencedTablesAsRowIndices(testCase)
5992
% The default mode for the toTable() method is to return the row indices
6093
% for dynamic table regions. This test verifies that the data type of

+types/+hdmf_common/CSRMatrix.m

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
% REQUIRED PROPERTIES
99
properties
1010
data; % REQUIRED (any) The non-zero values in the matrix.
11-
indices; % REQUIRED (uint) The column indices.
12-
indptr; % REQUIRED (uint) The row index pointer.
13-
shape; % REQUIRED (uint) The shape (number of rows, number of columns) of this sparse matrix.
11+
indices; % REQUIRED (uint8) The column indices.
12+
indptr; % REQUIRED (uint8) The row index pointer.
13+
shape; % REQUIRED (uint8) The shape (number of rows, number of columns) of this sparse matrix.
1414
end
1515

1616
methods
@@ -25,11 +25,11 @@
2525
% Input Arguments (Name-Value Arguments):
2626
% - data (any) - The non-zero values in the matrix.
2727
%
28-
% - indices (uint) - The column indices.
28+
% - indices (uint8) - The column indices.
2929
%
30-
% - indptr (uint) - The row index pointer.
30+
% - indptr (uint8) - The row index pointer.
3131
%
32-
% - shape (uint) - The shape (number of rows, number of columns) of this sparse matrix.
32+
% - shape (uint8) - The shape (number of rows, number of columns) of this sparse matrix.
3333
%
3434
% Output Arguments:
3535
% - cSRMatrix (types.hdmf_common.CSRMatrix) - A CSRMatrix object
@@ -77,15 +77,15 @@
7777
types.util.validateShape('data', {[Inf]}, val)
7878
end
7979
function val = validate_indices(obj, val)
80-
val = types.util.checkDtype('indices', 'uint', val);
80+
val = types.util.checkDtype('indices', 'uint8', val);
8181
types.util.validateShape('indices', {[Inf]}, val)
8282
end
8383
function val = validate_indptr(obj, val)
84-
val = types.util.checkDtype('indptr', 'uint', val);
84+
val = types.util.checkDtype('indptr', 'uint8', val);
8585
types.util.validateShape('indptr', {[Inf]}, val)
8686
end
8787
function val = validate_shape(obj, val)
88-
val = types.util.checkDtype('shape', 'uint', val);
88+
val = types.util.checkDtype('shape', 'uint8', val);
8989
types.util.validateShape('shape', {[2]}, val)
9090
end
9191
%% EXPORT

+types/+hdmf_experimental/HERD.m

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@
113113
% skip validation for empty values
114114
else
115115
vprops = struct();
116-
vprops.entities_idx = 'uint';
117-
vprops.keys_idx = 'uint';
116+
vprops.entities_idx = 'uint8';
117+
vprops.keys_idx = 'uint8';
118118
val = types.util.checkDtype('entity_keys', vprops, val);
119119
end
120120
types.util.validateShape('entity_keys', {[Inf]}, val)
@@ -159,8 +159,8 @@
159159
% skip validation for empty values
160160
else
161161
vprops = struct();
162-
vprops.objects_idx = 'uint';
163-
vprops.keys_idx = 'uint';
162+
vprops.objects_idx = 'uint8';
163+
vprops.keys_idx = 'uint8';
164164
val = types.util.checkDtype('object_keys', vprops, val);
165165
end
166166
types.util.validateShape('object_keys', {[Inf]}, val)
@@ -175,7 +175,7 @@
175175
% skip validation for empty values
176176
else
177177
vprops = struct();
178-
vprops.files_idx = 'uint';
178+
vprops.files_idx = 'uint8';
179179
vprops.object_id = 'char';
180180
vprops.object_type = 'char';
181181
vprops.relative_path = 'char';

0 commit comments

Comments
 (0)