diff --git a/+tests/+sanity/GenerationTest.m b/+tests/+sanity/GenerationTest.m index 1b30f54c..740e0efc 100644 --- a/+tests/+sanity/GenerationTest.m +++ b/+tests/+sanity/GenerationTest.m @@ -51,7 +51,7 @@ function dynamicTableMethodsTest(testCase) 'VariableNames', {'id', 'start_time', 'stop_time', 'randomvalues', 'stringdata'}); % verify error is thrown when addRow input is MATLAB table testCase.verifyError(@() TimeIntervals.addRow(t), ... - "NWB:DynamicTable" ... + 'NWB:DynamicTable:AddRow:TableIsUnsupported' ... ); retrievalIndex = round(1 + 4 .* rand(10, 1)); diff --git a/+tests/+system/DynamicTableTest.m b/+tests/+system/DynamicTableTest.m index 43dbe6f4..26d1ae21 100644 --- a/+tests/+system/DynamicTableTest.m +++ b/+tests/+system/DynamicTableTest.m @@ -105,25 +105,22 @@ function addContainerUndefinedIDs(~, file) c = file.intervals_trials.vectordata.get('randomvalues'); end - function appendContainer(testCase, file) - container = testCase.getContainer(file); + function appendContainer(testCase, File) + container = testCase.getContainer(File); container.data = rand(size(container.data)); % new random values. - file.intervals_trials.vectordata.get('stringdata').data = repmat({'FALSE'}, 20, 1); - %test adding new column with argument - file.intervals_trials.addColumn( ... - 'newcolumn', types.hdmf_common.VectorData( ... - 'description', 'newly added column', ... - 'data', (20:-1:1) .' ... - ) ... - ); + File.intervals_trials.vectordata.get('stringdata').data = repmat({'FALSE'}, 20, 1); + % test adding new column with argument + + Column = types.hdmf_common.VectorData( ... + 'description', 'newly added column' ... + , 'data', (20:-1:1) .'); + File.intervals_trials.addColumn('newcolumn', Column); % verify error is thrown when addRow input is MATLAB table - t = table( ... - (1:2:40)', (1:4:80)' , ... - 'VariableNames', {'newcolumn2', 'newcolumn3'} ... - ); - testCase.verifyError(@() file.intervals_trials.addColumn(t), ... - "NWB:DynamicTable" ... - ); + Table = table( ... + (1:2:40)', (1:4:80)' ... + , 'VariableNames', {'newcolumn2', 'newcolumn3'}); + testCase.verifyError(@()File.intervals_trials.addColumn(Table) ... + , 'NWB:DynamicTable:AddColumn:InvalidArgument'); end function appendRaggedContainer(~, file) @@ -138,12 +135,9 @@ function appendRaggedContainer(~, file) startInd = endInd+1; end % get corresponding VectorData and VectorIndex - [rag_col, rag_col_index] = util.create_indexed_column(dataArray); + [Column, Index] = util.create_indexed_column(dataArray); % append ragged column - file.intervals_trials.addColumn( ... - 'newraggedcolumn',rag_col, ... - 'newraggedcolumn_index',rag_col_index ... - ) + file.intervals_trials.addColumn('newraggedcolumn', Column, Index); end end diff --git a/+tests/+unit/dataPipeTest.m b/+tests/+unit/dataPipeTest.m index 63970472..e636cca2 100644 --- a/+tests/+unit/dataPipeTest.m +++ b/+tests/+unit/dataPipeTest.m @@ -64,43 +64,42 @@ function testInit(testCase) function testIndex(testCase) filename = 'testIndexing.h5'; name = '/test_data'; - + data = rand(100, 100, 100); Pipe = types.untyped.DataPipe('data', data); - + testCase.verifyEqual(Pipe(:), data(:)); testCase.verifyEqual(Pipe(:,:,1), data(:,:,1)); - + fid = H5F.create(filename); Pipe.export(fid, name, {}); % bind the pipe. H5F.close(fid); - + testCase.verifyEqual(Pipe(:), data(:)); testCase.verifyEqual(Pipe(:,:,1), data(:,:,1)); end function testAppend(testCase) filename = 'testIterativeWrite.h5'; - Pipe = types.untyped.DataPipe(... 'maxSize', [10 13 15],... 'axis', 3,... 'chunkSize', [10 13 1],... 'dataType', 'uint8',... 'compressionLevel', 5); - + OneDimensionPipe = types.untyped.DataPipe('maxSize', Inf, 'data', [7, 8, 9]); - + %% create test file fid = H5F.create(filename); - + initialData = createData(Pipe.dataType, [10 13 10]); Pipe.internal.data = initialData; Pipe.export(fid, '/test_data', {}); % bind OneDimensionPipe.export(fid, '/test_one_dim_data', {}); - + H5F.close(fid); - + %% append data totalLength = 3; appendData = zeros([10 13 totalLength], Pipe.dataType); @@ -108,17 +107,16 @@ function testAppend(testCase) appendData(:,:,i) = createData(Pipe.dataType, Pipe.chunkSize); Pipe.append(appendData(:,:,i)); end - + for i = 1:totalLength OneDimensionPipe.append(rand()); end - + %% verify data Pipe = types.untyped.DataPipe('filename', filename, 'path', '/test_data'); readData = Pipe.load(); testCase.verifyEqual(readData(:,:,1:10), initialData); testCase.verifyEqual(readData(:,:,11:end), appendData); - OneDimensionPipe = types.untyped.DataPipe('filename', filename, 'path', '/test_one_dim_data'); readData = OneDimensionPipe.load(); testCase.verifyTrue(isvector(readData)); @@ -130,30 +128,30 @@ function testExternalFilters(testCase) import types.untyped.datapipe.dynamic.Filter; import types.untyped.datapipe.properties.DynamicFilter; import types.untyped.datapipe.properties.Shuffle; - + testCase.assumeTrue(logical(H5Z.filter_avail(uint32(Filter.LZ4)))); - + filename = 'testExternalWrite.h5'; - + Pipe = types.untyped.DataPipe(... 'maxSize', [10 13 15],... 'axis', 3,... 'chunkSize', [10 13 1],... 'dataType', 'uint8',... 'filters', [Shuffle() DynamicFilter(Filter.LZ4)]); - + OneDimensionPipe = types.untyped.DataPipe('maxSize', Inf, 'data', [7, 8, 9]); - + %% create test file fid = H5F.create(filename); - + initialData = createData(Pipe.dataType, [10 13 10]); Pipe.internal.data = initialData; Pipe.export(fid, '/test_data', {}); % bind OneDimensionPipe.export(fid, '/test_one_dim_data', {}); - + H5F.close(fid); - + %% append data totalLength = 3; appendData = zeros([10 13 totalLength], Pipe.dataType); diff --git a/+types/+util/+dynamictable/addColumn.m b/+types/+util/+dynamictable/addColumn.m index 2ecf7625..b3547175 100644 --- a/+types/+util/+dynamictable/addColumn.m +++ b/+types/+util/+dynamictable/addColumn.m @@ -1,41 +1,43 @@ function addColumn(DynamicTable, varargin) -% ADDCOLUMN Given a dynamic table and a set of keyword arguments for one or -% more columns, add one or more columns to the dynamic table by providing -% either keywords or a MATLAB table -% -% ADDCOLUMN(DT,TABLE) append the columns of the MATLAB Table TABLE to the -% DynamicTable -% -% ADDCOLUMN(DT,col_name1,col_vector1,...,col_namen,col_vectorn) -% append specified column names and VectorData to table -% -% This function asserts the following: -% 1) DynamicTable is a valid dynamic table and has the correct -% properties. -% 2) The height of the columns to be appended matches the height of the -% existing columns + % ADDCOLUMN Given a dynamic table and a keyword argument, add a column to the dynamic table. + % + % ADDCOLUMN(DT,NM,VD) + % append specified column name NM and non-ragged VectorData VD to DynamicTable DT + % + % ADDCOLUMN(DT,NM, VD, VI) append specified column by col_name NM represented + % by multiple VectorIndex references VI ordered in such a way where VI(n) references V(n-1) and + % VI(1) references VectorData VD. + % + % This function asserts the following: + % 1) DynamicTable is a valid dynamic table and has the correct + % properties. + % 2) The height of the columns to be appended matches the height of the + % existing columns -validateattributes(DynamicTable,... - {'types.core.DynamicTable', 'types.hdmf_common.DynamicTable'},... - {'scalar'}); + validateattributes(DynamicTable ... + , {'types.core.DynamicTable', 'types.hdmf_common.DynamicTable'} ... + , {'scalar'}); -assert(nargin > 1, 'NWB:DynamicTable:AddColumn:NoData', 'Not enough arguments'); + assert(nargin > 1, 'NWB:DynamicTable:AddColumn:NoData', 'Not enough arguments'); -if isempty(DynamicTable.id) - if 8 == exist('types.hdmf_common.ElementIdentifiers', 'class') - DynamicTable.id = types.hdmf_common.ElementIdentifiers(); - else % legacy Element Identifiers - DynamicTable.id = types.core.ElementIdentifiers(); + if isempty(DynamicTable.id) + if 8 == exist('types.hdmf_common.ElementIdentifiers', 'class') + DynamicTable.id = types.hdmf_common.ElementIdentifiers(); + else % legacy Element Identifiers + DynamicTable.id = types.core.ElementIdentifiers(); + end end -end -assert(~isa(DynamicTable.id.data, 'types.untyped.DataStub'),... - 'NWB:DynamicTable:AddColumn:Uneditable',... - ['Cannot write to on-file Dynamic Tables without enabling data pipes. '... - 'If this was produced with pynwb, please enable chunking for this table.']); + assert(~isa(DynamicTable.id.data, 'types.untyped.DataStub') ... + , 'NWB:DynamicTable:AddColumn:Uneditable' ... + , [ ... + 'Cannot write to on-file Dynamic Tables without enabling data pipes. '... + 'If this was produced with pynwb, please enable chunking for this table.']); -if istable(varargin{1}) - types.util.dynamictable.addTableColumn(DynamicTable, varargin{:}); -else + assert(~istable(varargin{1}) ... + , 'NWB:DynamicTable:AddColumn:InvalidArgument' ... + , [ ... + 'Using MATLAB tables as input to the addColumn DynamicTable method has been deprecated. ' ... + 'Please, use key-value pairs instead.']); types.util.dynamictable.addVarargColumn(DynamicTable, varargin{:}); end diff --git a/+types/+util/+dynamictable/addRawData.m b/+types/+util/+dynamictable/addRawData.m index 43de6e4e..65f6f6cf 100644 --- a/+types/+util/+dynamictable/addRawData.m +++ b/+types/+util/+dynamictable/addRawData.m @@ -1,25 +1,25 @@ -function addRawData(DynamicTable, column, data) +function addRawData(DynamicTable, columnName, data) %ADDRAWDATA Internal method for adding data to DynamicTable given column % name and data. Indices are determined based on data format and available % indices. - validateattributes(column, {'char'}, {'scalartext'}); + validateattributes(columnName, {'char'}, {'scalartext'}); - if (isprop(DynamicTable, column) && isempty(DynamicTable.(column))) ... - || (~isprop(DynamicTable, column) && ~isKey(DynamicTable.vectordata, column)) + if (isprop(DynamicTable, columnName) && isempty(DynamicTable.(columnName))) ... + || (~isprop(DynamicTable, columnName) && ~isKey(DynamicTable.vectordata, columnName)) % No vecdata found anywhere. Initialize. - initVecData(DynamicTable, column, class(data)); + initVecData(DynamicTable, columnName, class(data)); end - if isprop(DynamicTable, column) - Vector = DynamicTable.(column); - elseif isprop(DynamicTable, 'vectorindex') && DynamicTable.vectorindex.isKey(column) - Vector = DynamicTable.vectorindex.get(column); + if isprop(DynamicTable, columnName) + Vector = DynamicTable.(columnName); + elseif isprop(DynamicTable, 'vectorindex') && DynamicTable.vectorindex.isKey(columnName) + Vector = DynamicTable.vectorindex.get(columnName); else - Vector = DynamicTable.vectordata.get(column); + Vector = DynamicTable.vectordata.get(columnName); end % grab all available indices for column. - indexChain = {column}; + indexChain = {columnName}; while true index = types.util.dynamictable.getIndex(DynamicTable, indexChain{end}); if isempty(index) @@ -35,10 +35,11 @@ function addRawData(DynamicTable, column, data) % find true nesting depth of column data. if isa(Vector.data, 'types.untyped.DataPipe') - depth = getNestedDataDepth(data, 'dataPipeDimension', Vector.data.axis); + depthRequestOptions = {'dataPipeDimension', Vector.data.axis}; else - depth = getNestedDataDepth(data); + depthRequestOptions = {}; end + depth = types.util.dynamictable.getDataDepth(data, depthRequestOptions{:}); % add indices until it matches depth. for iVec = (length(indexChain)+1):depth @@ -99,35 +100,6 @@ function initVecData(DynamicTable, column, dataType) end end -function depth = getNestedDataDepth(data, varargin) - p = inputParser; - p.addParameter('dataPipeDimension', [], @(x)isnumeric(x) && (isempty(x) || isscalar(x))); - p.parse(varargin{:}); - - depth = 1; - subData = data; - while iscell(subData) && ~iscellstr(subData) - depth = depth + 1; - subData = subData{1}; - end - - % special case where the final data is in fact multiple rows to begin - % with. - if isempty(p.Results.dataPipeDimension) - if ischar(subData) - isMultiRow = 1 < size(subData, 1); - else - isMultiRow = (ismatrix(subData) && 1 < size(subData, 2)) ... - || (isvector(subData) && 1 < length(subData)); - end - else - isMultiRow = 1 < size(subData, p.Results.dataPipeDimension); - end - if isMultiRow - depth = depth + 1; - end -end - function numRows = nestedAdd(DynamicTable, indChain, data) name = indChain{1}; diff --git a/+types/+util/+dynamictable/addRow.m b/+types/+util/+dynamictable/addRow.m index 3afceb70..85d188f4 100644 --- a/+types/+util/+dynamictable/addRow.m +++ b/+types/+util/+dynamictable/addRow.m @@ -1,49 +1,50 @@ function addRow(DynamicTable, varargin) -% ADDROW Given a dynamic table and a set of keyword arguments for the row, -% add a single row to the dynamic table if using keywords, or multiple rows -% if using a table. -% -% ADDROW(DT,table) append the MATLAB table to the DynamicTable -% -% ADDROW(DT,col1,val1,col2,val2,...,coln,valn) append a single row -% to the DynamicTable -% -% ADDROW(DT,___,Name,Value) optional 'id' -% -% This function asserts the following: -% 1) DynamicTable is a valid dynamic table and has the correct -% properties. -% 2) The given keyword argument names match one of those ALREADY specified -% by the DynamicTable (that is, colnames MUST be filled out). -% 3) If the dynamic table is non-empty, the types of the column value MUST -% match the keyword value. -% 4) All horizontal data must match the width of the rest of the rows. -% Variable length strings should use cell arrays each row. -% 5) The type of the data cannot be a cell array of numeric values if using -% keyword arguments. For table appending mode, this is how ragged arrays -% are represented. + % ADDROW Given a dynamic table and a set of keyword arguments for the row, + % add a single row to the dynamic table if using keywords, or multiple rows + % if using a table. + % + % ADDROW(DT,table) append the MATLAB table to the DynamicTable + % + % ADDROW(DT,col1,val1,col2,val2,...,coln,valn) append a single row + % to the DynamicTable + % + % ADDROW(DT,___,Name,Value) optional 'id' + % + % This function asserts the following: + % 1) DynamicTable is a valid dynamic table and has the correct + % properties. + % 2) The given keyword argument names match one of those ALREADY specified + % by the DynamicTable (that is, colnames MUST be filled out). + % 3) If the dynamic table is non-empty, the types of the column value MUST + % match the keyword value. + % 4) All horizontal data must match the width of the rest of the rows. + % Variable length strings should use cell arrays each row. + % 5) The type of the data cannot be a cell array of numeric values if using + % keyword arguments. For table appending mode, this is how ragged arrays + % are represented. -validateattributes(DynamicTable,... - {'types.core.DynamicTable', 'types.hdmf_common.DynamicTable'},... - {'scalar'}); -assert(~isempty(DynamicTable.colnames),... - 'NWB:DynamicTable:AddRow:NoColumns',... - ['The `colnames` property of the Dynamic Table needs to be populated with a cell array '... - 'of column names before being able to add row data.']); -assert(nargin > 1, 'NWB:DynamicTable:AddRow:NoData', 'Not enough arguments'); + validateattributes(DynamicTable ... + , {'types.core.DynamicTable', 'types.hdmf_common.DynamicTable'} ... + , {'scalar'}); + assert(~isempty(DynamicTable.colnames)... + , 'NWB:DynamicTable:AddRow:NoColumns'... + , [ ... + 'The `colnames` property of the Dynamic Table needs to be populated with a cell array ' ... + 'of column names before being able to add row data.']); + assert(nargin > 1, 'NWB:DynamicTable:AddRow:NoData', 'Not enough arguments'); -types.util.dynamictable.checkConfig(DynamicTable); + types.util.dynamictable.checkConfig(DynamicTable); -assert(~isa(DynamicTable.id.data, 'types.untyped.DataStub'),... - 'NWB:DynamicTable:AddRow:Uneditable',... - ['Cannot write to on-file Dynamic Tables without enabling data pipes. '... - 'If this was produced with pynwb, please enable chunking for this table.']); + assert(~isa(DynamicTable.id.data, 'types.untyped.DataStub')... + , 'NWB:DynamicTable:AddRow:Uneditable'... + , [ ... + 'Cannot write to on-file Dynamic Tables without enabling data pipes. '... + 'If this was produced with pynwb, please enable chunking for this table.']); -if istable(varargin{1}) - error("NWB:DynamicTable", ... - ['Using MATLAB tables as input to the addRow DynamicTable method has '... - 'been deprecated. Please, use key-value pairs instead']); -else + assert(~istable(varargin{1}) ... + , 'NWB:DynamicTable:AddRow:TableIsUnsupported' ... + , [ ... + 'Using MATLAB tables as input to the addRow DynamicTable method has '... + 'been deprecated. Please, use key-value pairs instead']); types.util.dynamictable.addVarargRow(DynamicTable, varargin{:}); -end end \ No newline at end of file diff --git a/+types/+util/+dynamictable/addTableColumn.m b/+types/+util/+dynamictable/addTableColumn.m deleted file mode 100644 index 38503809..00000000 --- a/+types/+util/+dynamictable/addTableColumn.m +++ /dev/null @@ -1,4 +0,0 @@ -function addTableColumn(DynamicTable, subTable) -error("NWB:DynamicTable", ... - ['Using MATLAB tables as input to the addColumn DynamicTable method has '... - 'been deprecated. Please, use key-value pairs instead']) \ No newline at end of file diff --git a/+types/+util/+dynamictable/addVarargColumn.m b/+types/+util/+dynamictable/addVarargColumn.m index 9484b54e..a2deb6ab 100644 --- a/+types/+util/+dynamictable/addVarargColumn.m +++ b/+types/+util/+dynamictable/addVarargColumn.m @@ -1,55 +1,130 @@ -function addVarargColumn(DynamicTable, varargin) - -%parse inputs -p = inputParser(); -p.KeepUnmatched = true; -p.StructExpand = false; -parse(p, varargin{:}); -newColNames = DynamicTable.validate_colnames(fieldnames(p.Unmatched)); -newVectorData = p.Unmatched; -% get current table height - assume id length reflects table height -if ~isempty(DynamicTable.colnames) - tableHeight = length(DynamicTable.id.data); -end +function addVarargColumn(DynamicTable, columnName, VectorData, VectorIndices) + validateattributes(columnName, {'char', 'string'}, {'scalartext'} ... + , 'addVarargColumn', 'columnName'); + + columnName = DynamicTable.validate_colnames(convertStringsToChars(columnName)); + + validateattributes(VectorData ... + , {'types.hdmf_common.VectorData', 'types.core.VectorData'}, {'scalar'} ... + , 'addVarargColumn', 'VectorData'); + + if 4 <= nargin + validateattributes(VectorIndices ... + , {'types.hdmf_common.VectorIndex', 'types.core.VectorIndex'}, {'vector'} ... + , 'addVarargColumn', 'VectorIndices'); + validateIndexOrder(VectorIndices, VectorData); + else + VectorIndices = []; + end + + if isempty(VectorIndices) + columnHeight = getColumnDataHeight(VectorData.data); + else + columnHeight = getColumnDataHeight(VectorIndices(end).data); + end -for i = 1:length(newColNames) - new_cn = newColNames{i}; - new_cv = newVectorData.(new_cn); - % check height match before adding column if ~isempty(DynamicTable.colnames) - indexName = getIndexInSet(newVectorData,new_cn); - if isempty(indexName) - assert(height(new_cv.data) == tableHeight,... - 'NWB:DynamicTable:AddColumn:MissingRows',... - 'New column length must match length of existing columns ') + % get current table height assuming id length is valid. Do not check for empty tables. + if isempty(DynamicTable.id) + tableHeight = 0; else - assert(height(newVectorData.(indexName).data) == tableHeight,... - 'NWB:DynamicTable:AddColumn:MissingRows',... - 'New column length must match length of existing columns ') + tableHeight = length(DynamicTable.id.data); end + assert(columnHeight == tableHeight ... + , 'NWB:DynamicTable:AddColumn:MissingRows' ... + , [ ... + 'Height of column "%s" (with height %d) does not match the pre-existant table height %d. ' ... + 'This function no longer supports nested data objects, please use "addRow" for each ' ... + 'nested object instead.'] ... + , columnName, columnHeight, tableHeight); + end + + if isempty(DynamicTable.colnames) + DynamicTable.colnames = {columnName}; + else + DynamicTable.colnames{end+1} = columnName; + end + addToDynamicTable(DynamicTable, columnName, VectorData); + for iIndex = 1:length(VectorIndices) + Index = VectorIndices(iIndex); + indexName = sprintf('%s%s', columnName, repmat('_index', 1, iIndex)); + addToDynamicTable(DynamicTable, indexName, Index); + end +end + +function addToDynamicTable(DynamicTable, name, Vector) + if isprop(DynamicTable, name) + DynamicTable.(name) = Vector; + return; end - if 8 == exist('types.hdmf_common.VectorIndex', 'class') - if ~isa(new_cv, 'types.hdmf_common.VectorIndex') - DynamicTable.colnames{end+1} = new_cn; + + [~, indexClassName] = types.util.getVectorClassName(); + if isa(Vector, indexClassName) && isprop(DynamicTable, 'vectorindex') + addVector = @(name, I)DynamicTable.vectorindex.set(name, I); + else + addVector = @(name, V)DynamicTable.vectordata.set(name, V); + end + addVector(name, Vector); +end + +function validateIndexOrder(VectorIndices, VectorData) + isValidationAmbiguous = false; + for iIndex = 1:length(VectorIndices) + Index = VectorIndices(iIndex); + ObjectView = Index.target; + + if 1 == iIndex + ExpectedTarget = VectorData; + else + ExpectedTarget = VectorIndices(iIndex - 1); end - else %legacy case - if ~isa(new_cv, 'types.core.VectorIndex') - DynamicTable.colnames{end+1} = new_cn; + + if isempty(ObjectView) + Index.target = types.untyped.ObjectView(ExpectedTarget); + continue; end + + assert(~isempty(ObjectView) ... + , 'NWB:DynamicTable:AddColumn:InvalidIndex' ... + , ['VectorIndex objects must be pointed either to their predecessor VectorIndex or ' ... + 'the provided VectorData object.']); + Target = ObjectView.target; + if isempty(Target) + isValidationAmbiguous = true; + continue; + end + + assert(ExpectedTarget == Target ... + , 'NWB:DynamicTable:AddColumn:InvalidIndexTarget' ... + , [ ... + 'VectorIndices argument must be ordered such that VectorIndices(N) points to ' ... + 'VectorIndices(N-1) and VectorIndices(1) points to VectorData']); + end + if isValidationAmbiguous + warningId = 'NWB:DynamicTable:AddColumn:UnverifiableIndexTarget'; + warning(warningId ... + , ['Some of the Vector Indices could not have their reference target verified as ' ... + 'they do not contain object targets. If not verified by the user, this may cause ' ... + 'validation errors when added to the DynamicTable object. You can suppress this ' ... + 'warning by running "%1$s" in the commnd window or clicking here.'] ... + , sprintf('warning(''off'',''%s'');', warningId)); end - DynamicTable.vectordata.set(new_cn, new_cv); -end end -function indexName = getIndexInSet(inputStruct, inputName) - % wrap input set with an empty dynamic table - T = types.hdmf_common.DynamicTable(); - % convert input structure to a set - columnNames = fieldnames(inputStruct); - for i = 1:length(columnNames) - T.vectordata.set(columnNames{i},inputStruct.(columnNames{i})); +function v = getColumnDataHeight(columnData) + if ischar(columnData) + v = size(columnData, 1); + elseif isa(columnData, 'types.untyped.DataPipe') + v = columnData.offset; + elseif isa(columnData, 'types.untyped.DataStub') + newDims = columnData.dims; + if isempty(newDims) + v = 0; + else + v = newDims(1); + end + else + v = length(columnData); end - % use dynamic table function to get index name - indexName = types.util.dynamictable.getIndex(T, inputName); end diff --git a/+types/+util/+dynamictable/addVarargRow.m b/+types/+util/+dynamictable/addVarargRow.m index c8c72950..b988b31c 100644 --- a/+types/+util/+dynamictable/addVarargRow.m +++ b/+types/+util/+dynamictable/addVarargRow.m @@ -1,48 +1,48 @@ function addVarargRow(DynamicTable, varargin) - p = inputParser(); - p.KeepUnmatched = true; - p.StructExpand = false; - addParameter(p, 'id', []); % `id` override but doesn't actually show up in `colnames` + Parser = inputParser(); + Parser.KeepUnmatched = true; + Parser.StructExpand = false; + addParameter(Parser, 'id', []); % `id` override but doesn't actually show up in `colnames` for iColumn = 1:length(DynamicTable.colnames) - addParameter(p, DynamicTable.colnames{iColumn}, []); + addParameter(Parser, DynamicTable.colnames{iColumn}, []); end - parse(p, varargin{:}); + parse(Parser, varargin{:}); - assert(isempty(fieldnames(p.Unmatched)),... - 'NWB:DynamicTable:AddRow:InvalidColumns',... - 'Invalid column name(s) { %s }', strjoin(fieldnames(p.Unmatched), ', ')); + assert(isempty(fieldnames(Parser.Unmatched))... + , 'NWB:DynamicTable:AddRow:InvalidColumns'... + , 'Invalid column name(s) { %s }', strjoin(fieldnames(Parser.Unmatched), ', ')); - rowNames = fieldnames(p.Results); + rowNames = fieldnames(Parser.Results); % not using setDiff because we want to retain set order. rowNames(strcmp(rowNames, 'id')) = []; - missingColumns = setdiff(p.UsingDefaults, {'id'}); + missingColumns = setdiff(Parser.UsingDefaults, {'id'}); assert(isempty(missingColumns),... 'NWB:DynamicTable:AddRow:MissingColumns',... 'Missing columns { %s }', strjoin(missingColumns, ', ')); - specifiesId = ~any(strcmp(p.UsingDefaults, 'id')); + specifiesId = ~any(strcmp(Parser.UsingDefaults, 'id')); if specifiesId - validateattributes(p.Results.id, {'numeric'}, {'scalar'}); + validateattributes(Parser.Results.id, {'numeric'}, {'scalar'}); end TypeMap = types.util.dynamictable.getTypeMap(DynamicTable); for iRow = 1:length(rowNames) - rn = rowNames{iRow}; - rv = p.Results.(rn); + rowName = rowNames{iRow}; + rowValue = Parser.Results.(rowName); - if isKey(TypeMap, rn) - validateType(TypeMap(rn), rv); + if isKey(TypeMap, rowName) + validateType(TypeMap(rowName), rowValue); end - types.util.dynamictable.addRawData(DynamicTable, rn, rv); + types.util.dynamictable.addRawData(DynamicTable, rowName, rowValue); end if specifiesId - newId = p.Results.id; + newId = Parser.Results.id; elseif isa(DynamicTable.id.data, 'types.untyped.DataPipe') newId = DynamicTable.id.data.offset; else @@ -56,16 +56,18 @@ function addVarargRow(DynamicTable, varargin) end end -function validateType(TypeStruct, rv) +function validateType(TypeStruct, rowValue) if strcmp(TypeStruct.type, 'cellstr') - assert(iscellstr(rv) || (ischar(rv) && (isempty(rv) || 1 == size(rv, 1))),... + assert(isstring(rowValue) ... + || iscellstr(rowValue) ... + || (ischar(rowValue) && (isempty(rowValue) || 1 == size(rowValue, 1))),... 'NWB:DynamicTable:AddRow:InvalidType',... 'Type of value must be a cell array of character vectors or a scalar character'); - elseif iscell(rv) - for iVal = 1:length(rv) - validateType(TypeStruct, rv{iVal}); + elseif iscell(rowValue) + for iVal = 1:length(rowValue) + validateType(TypeStruct, rowValue{iVal}); end else - validateattributes(rv, {TypeStruct.type}, {}); + validateattributes(rowValue, {TypeStruct.type}, {}); end end diff --git a/+types/+util/+dynamictable/getDataDepth.m b/+types/+util/+dynamictable/getDataDepth.m new file mode 100644 index 00000000..5fd7e92d --- /dev/null +++ b/+types/+util/+dynamictable/getDataDepth.m @@ -0,0 +1,32 @@ +function depth = getDataDepth(candidate, varargin) + %GETDATADEPTH given candidate column or row data, returns the depth (number of indices required) + %to represent it in a Dynamic Table object. + + Parser = inputParser; + Parser.addParameter('dataPipeDimension', [], @(x)isnumeric(x) && (isempty(x) || isscalar(x))); + Parser.parse(varargin{:}); + + depth = 1; + subData = candidate; + while iscell(subData) && ~iscellstr(subData) + depth = depth + 1; + subData = subData{1}; + end + + % special case where the final data is in fact multiple rows to begin + % with. + if isempty(Parser.Results.dataPipeDimension) + if ischar(subData) + isMultiRow = 1 < size(subData, 1); + else + isMultiRow = (ismatrix(subData) && 1 < size(subData, 2)) ... + || (isvector(subData) && 1 < length(subData)); + end + else + isMultiRow = 1 < size(subData, Parser.Results.dataPipeDimension); + end + if isMultiRow + depth = depth + 1; + end +end + diff --git a/+types/+util/+dynamictable/getIndex.m b/+types/+util/+dynamictable/getIndex.m index 7e88b17f..0936a267 100644 --- a/+types/+util/+dynamictable/getIndex.m +++ b/+types/+util/+dynamictable/getIndex.m @@ -1,73 +1,72 @@ -function indexName = getIndex(DynamicTable, column) -%GETINDEX Given a dynamic table and its column name, get its VectorIndex column name -validateattributes(DynamicTable,... - {'types.core.DynamicTable', 'types.hdmf_common.DynamicTable'},... - {'scalar'}); -validateattributes(column, {'char'}, {'scalartext'}); -indexName = ''; -if strcmp(column, 'id') - return; -end +function indexName = getIndex(DynamicTable, columnName) + %GETINDEX Given a dynamic table and its column name, get its VectorIndex column name + validateattributes(DynamicTable,... + {'types.core.DynamicTable', 'types.hdmf_common.DynamicTable'},... + {'scalar'}); + validateattributes(columnName, {'char'}, {'scalartext'}); + indexName = ''; + if strcmp(columnName, 'id') + return; + end -% after Schema version 2.3.0, VectorIndex objects subclass VectorData which -% meant that vectorindex and vectordata sets could be combined. -isLegacyDynamicTable = isprop(DynamicTable, 'vectorindex'); -if isLegacyDynamicTable - vecKeys = keys(DynamicTable.vectorindex); -else - vecKeys = keys(DynamicTable.vectordata); -end -for i = 1:length(vecKeys) - vk = vecKeys{i}; + % after Schema version 2.3.0, VectorIndex objects subclass VectorData which + % meant that vectorindex and vectordata sets could be combined. + isLegacyDynamicTable = isprop(DynamicTable, 'vectorindex'); if isLegacyDynamicTable - vecData = DynamicTable.vectorindex.get(vk); + vectorKeys = keys(DynamicTable.vectorindex); else - vecData = DynamicTable.vectordata.get(vk); - end - if ~isa(vecData, 'types.hdmf_common.VectorIndex')... - && ~isa(vecData, 'types.core.VectorIndex') - continue; + vectorKeys = keys(DynamicTable.vectordata); end - if isVecIndColumn(DynamicTable, vecData, column) - indexName = vk; - return; + for iKey = 1:length(vectorKeys) + tableProperties = vectorKeys{iKey}; + if isLegacyDynamicTable + Vector = DynamicTable.vectorindex.get(tableProperties); + else + Vector = DynamicTable.vectordata.get(tableProperties); + end + if ~isa(Vector, 'types.hdmf_common.VectorIndex') && ~isa(Vector, 'types.core.VectorIndex') + continue; + end + if isVecIndColumn(DynamicTable, Vector, columnName) + indexName = tableProperties; + return; + end end -end -% check if dynamic table object has extended properties which point to -% vector indices. These are specifically defined by the schema to be -% properties. -DynamicTableProps = properties(DynamicTable); -isPropVecInd = false(size(DynamicTableProps)); -for i = 1:length(DynamicTableProps) - PropVec = DynamicTable.(DynamicTableProps{i}); - isPropVecInd(i) = isa(PropVec, 'types.hdmf_common.VectorIndex')... - || isa(PropVec, 'types.core.VectorIndex'); -end + % check if dynamic table object has extended properties which point to + % vector indices. These are specifically defined by the schema to be + % properties. + DynamicTableProps = properties(DynamicTable); + isPropVecInd = false(size(DynamicTableProps)); + for i = 1:length(DynamicTableProps) + PropVec = DynamicTable.(DynamicTableProps{i}); + isPropVecInd(i) = isa(PropVec, 'types.hdmf_common.VectorIndex')... + || isa(PropVec, 'types.core.VectorIndex'); + end -DynamicTableProps = DynamicTableProps(isPropVecInd); -for i = 1:length(DynamicTableProps) - vk = DynamicTableProps{i}; - VecInd = DynamicTable.(vk); - if isVecIndColumn(DynamicTable, VecInd, column) - indexName = vk; - return; + DynamicTableProps = DynamicTableProps(isPropVecInd); + for i = 1:length(DynamicTableProps) + tableProperties = DynamicTableProps{i}; + VecInd = DynamicTable.(tableProperties); + if isVecIndColumn(DynamicTable, VecInd, columnName) + indexName = tableProperties; + return; + end end end -end function tf = isVecIndColumn(DynamicTable, VectorIndex, column) -if VectorIndex.target.has_path() - tf = endsWith(VectorIndex.target.path, ['/' column]); -elseif isprop(DynamicTable, column) - tf = VectorIndex.target.target == DynamicTable.(column); -else - if isprop(DynamicTable, 'vectorindex') && DynamicTable.vectorindex.isKey(column) - Vec = DynamicTable.vectorindex.get(column); + if VectorIndex.target.has_path() + tf = endsWith(VectorIndex.target.path, ['/' column]); + elseif isprop(DynamicTable, column) + tf = VectorIndex.target.target == DynamicTable.(column); else - Vec = DynamicTable.vectordata.get(column); + if isprop(DynamicTable, 'vectorindex') && DynamicTable.vectorindex.isKey(column) + Vec = DynamicTable.vectorindex.get(column); + else + Vec = DynamicTable.vectordata.get(column); + end + tf = VectorIndex.target.target == Vec; end - tf = VectorIndex.target.target == Vec; -end end diff --git a/+types/+util/correctType.m b/+types/+util/correctType.m index 632b3e0f..a4228b0c 100644 --- a/+types/+util/correctType.m +++ b/+types/+util/correctType.m @@ -13,10 +13,7 @@ errorMessage = sprintf(errorTemplate, ... sprintf('value was not a valid string type. got %s instead', class(val)) ... ); - assert(isstring(val) || ischar(val) || iscellstr(val), ... - errorId, ... - errorMessage ... - ); + assert(isstring(val) || ischar(val) || iscellstr(val), errorId, errorMessage); case 'datetime' isCellString = iscellstr(val) || (iscell(val) && all(cellfun('isclass', val, 'string'))); isCellDatetime = iscell(val) && all(cellfun('isclass', val, 'datetime')); diff --git a/+types/+util/getVectorClassName.m b/+types/+util/getVectorClassName.m new file mode 100644 index 00000000..48196eec --- /dev/null +++ b/+types/+util/getVectorClassName.m @@ -0,0 +1,44 @@ +function [dataName, indexName] = getVectorClassName() + % GETVECTORCLASSNAME returns applicable class names from the MATLAB environment. + % + % D = GETVECTORCLASSNAME() returns the correct VectorData class name, i.e. + % 'types.hdmf_common.VectorData' + % + % [D, I] = GETVECTORCLASSNAME() returns the VectorData class name D and VectorIndex class name + % I. i.e. 'types.hdmf_common.VectorData' and 'types.hdmf_common.VectorIndex' + % + % Throws an error if no classes are found. + % Emits a warning if the namespaces for VectorIndex and VectorData do not match. + + dataName = ''; + indexName = ''; + + if 8 == exist('types.hdmf_common.VectorData', 'class') + dataName = 'types.hdmf_common.VectorData'; + elseif 8 == exist('types.core.VectorData', 'class') + dataName = 'types.core.VectorData'; + end + + if 8 == exist('types.hdmf_common.VectorIndex', 'class') + indexName = 'types.hdmf_common.VectorIndex'; + elseif 8 == exist('types.core.VectorIndex', 'class') + indexName = 'types.core.VectorIndex'; + end + + assert(~isempty(dataName) && ~isempty(indexName) ... + , 'NWB:GetVectorClassName:MissingNamespace' ... + , 'Could not find any useable VectorData or VectorIndex class names.'); + + dataPackages = split(dataName, '.'); + dataNamespace = dataPackages{2}; + indexPackages = split(indexName, '.'); + indexNamespace = indexPackages{2}; + if ~strcmp(dataNamespace, indexNamespace) + warning('NWB:GetVectorClassName:PotentiallyInvalidNamespace' ... + , [ ... + 'VectorData namespace "%s" is not the same as VectorIndex namespace "%s". ' ... + 'Please ensure your generated namespace files are not from a previous error.'] ... + , dataNamespace, indexNamespace); + end +end + diff --git a/tutorials/dynamic_tables.mlx b/tutorials/dynamic_tables.mlx index 35411bdf..d65c9a09 100644 Binary files a/tutorials/dynamic_tables.mlx and b/tutorials/dynamic_tables.mlx differ diff --git a/tutorials/html/dynamic_tables.html b/tutorials/html/dynamic_tables.html index 1549783e..25bb8d5b 100644 --- a/tutorials/html/dynamic_tables.html +++ b/tutorials/html/dynamic_tables.html @@ -6,11 +6,11 @@ .S4 { margin: -1px 0px 0px; padding: 10px 0px 10px 7px; line-height: 21px; min-height: 0px; white-space: pre-wrap; color: rgb(33, 33, 33); font-family: Helvetica, Arial, sans-serif; font-style: normal; font-size: 14px; font-weight: 400; text-align: start; } .S5 { margin: 20px 10px 5px 4px; padding: 0px; line-height: 20px; min-height: 0px; white-space: pre-wrap; color: rgb(33, 33, 33); font-family: Helvetica, Arial, sans-serif; font-style: normal; font-size: 20px; font-weight: 700; text-align: left; } .CodeBlock { background-color: #F5F5F5; margin: 10px 0 10px 0; } -.S6 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 1px solid rgb(217, 217, 217); border-bottom: 0px none rgb(33, 33, 33); border-radius: 4px 4px 0px 0px; padding: 6px 45px 0px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } -.S7 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 0px none rgb(33, 33, 33); border-bottom: 0px none rgb(33, 33, 33); border-radius: 0px; padding: 0px 45px 0px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } -.S8 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 0px none rgb(33, 33, 33); border-bottom: 1px solid rgb(217, 217, 217); border-radius: 0px 0px 4px 4px; padding: 0px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } +.S6 { border-left: 0.998264px solid rgb(217, 217, 217); border-right: 0.998264px solid rgb(217, 217, 217); border-top: 0.998264px solid rgb(217, 217, 217); border-bottom: 0px none rgb(33, 33, 33); border-radius: 4px 4px 0px 0px; padding: 6px 45px 0px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } +.S7 { border-left: 0.998264px solid rgb(217, 217, 217); border-right: 0.998264px solid rgb(217, 217, 217); border-top: 0px none rgb(33, 33, 33); border-bottom: 0px none rgb(33, 33, 33); border-radius: 0px; padding: 0px 45px 0px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } +.S8 { border-left: 0.998264px solid rgb(217, 217, 217); border-right: 0.998264px solid rgb(217, 217, 217); border-top: 0px none rgb(33, 33, 33); border-bottom: 0.998264px solid rgb(217, 217, 217); border-radius: 0px 0px 4px 4px; padding: 0px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } .S9 { margin: 3px 10px 5px 4px; padding: 0px; line-height: 20px; min-height: 0px; white-space: pre-wrap; color: rgb(33, 33, 33); font-family: Helvetica, Arial, sans-serif; font-style: normal; font-size: 20px; font-weight: 700; text-align: left; } -.S10 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 0px none rgb(33, 33, 33); border-bottom: 1px solid rgb(217, 217, 217); border-radius: 0px; padding: 0px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } +.S10 { border-left: 0.998264px solid rgb(217, 217, 217); border-right: 0.998264px solid rgb(217, 217, 217); border-top: 0px none rgb(33, 33, 33); border-bottom: 0.998264px solid rgb(217, 217, 217); border-radius: 0px; padding: 0px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } .S11 { color: rgb(33, 33, 33); padding: 10px 0px 6px 17px; background: rgb(255, 255, 255) none repeat scroll 0% 0% / auto padding-box border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; overflow-x: hidden; line-height: 17.234px; } /* Styling that is common to warnings and errors is in diagnosticOutput.css */.embeddedOutputsErrorElement { min-height: 18px; max-height: 550px;} .embeddedOutputsErrorElement .diagnosticMessage-errorType { overflow: auto;} @@ -36,14 +36,14 @@ .inlineElement .textElement {} .embeddedOutputsTextElement.rightPaneElement,.embeddedOutputsVariableStringElement.rightPaneElement { min-height: 16px;} .rightPaneElement .textElement { padding-top: 2px; padding-left: 9px;} -.S12 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 1px solid rgb(217, 217, 217); border-bottom: 1px solid rgb(217, 217, 217); border-radius: 4px; padding: 6px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } +.S12 { border-left: 0.998264px solid rgb(217, 217, 217); border-right: 0.998264px solid rgb(217, 217, 217); border-top: 0.998264px solid rgb(217, 217, 217); border-bottom: 0.998264px solid rgb(217, 217, 217); border-radius: 4px; padding: 6px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } .S13 { margin: 15px 10px 5px 4px; padding: 0px; line-height: 18px; min-height: 0px; white-space: pre-wrap; color: rgb(33, 33, 33); font-family: Helvetica, Arial, sans-serif; font-style: normal; font-size: 17px; font-weight: 700; text-align: left; } .embeddedOutputsVariableTableElement .ClientViewDiv table tr { height: 22px; white-space: nowrap;} .embeddedOutputsVariableTableElement .ClientViewDiv table tr td,.embeddedOutputsVariableTableElement .ClientViewDiv table tr th { background-color:white; text-overflow: ellipsis; font-family: 'Arial', sans-serif; font-size: 12px; overflow : hidden;} .embeddedOutputsVariableTableElement .ClientViewDiv table tr span { text-overflow: ellipsis; padding: 3px;} .embeddedOutputsVariableTableElement .ClientViewDiv table tr th { color: rgba(0,0,0,0.5); padding: 3px; font-size: 9px;} /* ClientDocument has a summary bar child that takes up 17px, this clashes with overflow on the view which allots space for scrollbars. On print preview, this causes headers from to overlap on subsequent pages. Displaying Document as flex renders summarybar and view in column format and fixes the issue g2788485 */.embeddedOutputsVariableTableElement .ClientDocument { display: flex; flex-direction: column;} -.S14 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 1px solid rgb(217, 217, 217); border-bottom: 1px solid rgb(217, 217, 217); border-radius: 4px 4px 0px 0px; padding: 6px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } +.S14 { border-left: 0.998264px solid rgb(217, 217, 217); border-right: 0.998264px solid rgb(217, 217, 217); border-top: 0.998264px solid rgb(217, 217, 217); border-bottom: 0.998264px solid rgb(217, 217, 217); border-radius: 4px 4px 0px 0px; padding: 6px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } .S15 { margin: 10px 10px 9px 4px; padding: 0px; line-height: 21px; min-height: 0px; white-space: pre-wrap; color: rgb(33, 33, 33); font-family: Helvetica, Arial, sans-serif; font-style: normal; font-size: 14px; font-weight: 400; text-align: left; } .embeddedOutputsMatrixElement,.eoOutputWrapper .matrixElement { min-height: 18px; box-sizing: border-box;} .embeddedOutputsMatrixElement .matrixElement,.eoOutputWrapper .matrixElement,.rtcDataTipElement .matrixElement { position: relative;} @@ -69,41 +69,41 @@ .outputsOnRight .embeddedOutputsVariableElement.rightPaneElement .eoOutputContent { /* Remove extra space allocated for navigation border */ margin-top: 0; margin-bottom: 0;} .variableNameElement { margin-bottom: 3px; display: inline-block;} /* * Ellipses as base64 for HTML export. */.matrixElement .horizontalEllipsis,.rtcDataTipElement .matrixElement .horizontalEllipsis { display: inline-block; margin-top: 3px; /* base64 encoded version of images-liveeditor/HEllipsis.png */ width: 30px; height: 12px; background-repeat: no-repeat; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAJCAYAAADO1CeCAAAAJUlEQVR42mP4//8/A70xw0i29BUDFPxnAEtTW37wWDqakIa4pQDvOOG89lHX2gAAAABJRU5ErkJggg==");} -.matrixElement .verticalEllipsis,.textElement .verticalEllipsis,.rtcDataTipElement .matrixElement .verticalEllipsis,.rtcDataTipElement .textElement .verticalEllipsis { margin-left: 35px; /* base64 encoded version of images-liveeditor/VEllipsis.png */ width: 12px; height: 30px; background-repeat: no-repeat; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAZCAYAAAAIcL+IAAAALklEQVR42mP4//8/AzGYgWyFMECMwv8QddRS+P//KyimlmcGUOFoOI6GI/UVAgDnd8Dd4+NCwgAAAABJRU5ErkJggg==");}

DynamicTables Tutorial

This is a user guide to interacting with DynamicTable objects in MatNWB.
Table of Contents
MatNWB Setup -Constructing a table with initialized columns -Adding rows -Adding columns +.matrixElement .verticalEllipsis,.textElement .verticalEllipsis,.rtcDataTipElement .matrixElement .verticalEllipsis,.rtcDataTipElement .textElement .verticalEllipsis { margin-left: 35px; /* base64 encoded version of images-liveeditor/VEllipsis.png */ width: 12px; height: 30px; background-repeat: no-repeat; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAZCAYAAAAIcL+IAAAALklEQVR42mP4//8/AzGYgWyFMECMwv8QddRS+P//KyimlmcGUOFoOI6GI/UVAgDnd8Dd4+NCwgAAAABJRU5ErkJggg==");}

DynamicTables Tutorial

MatNWB Setup

Start by setting up your MATLAB workspace. The code below adds the directory containing the MatNWB package to the MATLAB search path. MatNWB works by automatically creating API classes based on a defined schema.
%{
path_to_matnwb = '~/Repositories/matnwb'; % change to your own path location
addpath(genpath(pwd));
%}

Constructing a table with initialized columns

The DynamicTable class represents a column-based table to which you can add custom columns. It consists of a description, a list of columns , and a list of row IDs. You can create a DynamicTable by first defining the VectorData objects that will make up the columns of the table. Each VectorData object must contain the same number of rows. A list of rows IDs may be passed to the DynamicTable using the id argument. Row IDs are a useful way to access row information independent of row location index. The list of row IDs must be cast as an ElementIdentifiers object before being passed to the DynamicTable object. If no value is passed to id, an ElementIdentifiers object with 0-indexed row IDs will be created for you automatically.
MATLAB Syntax Note: Using column vectors is crucial to properly build vectors and tables. When defining individual values, make sure to use semi-colon (;) instead of instead of comma (,) when defining the data fields of these.
col1 = types.hdmf_common.VectorData( ...
'description', 'column #1', ...
'data', [1;2] ...
);
 
col2 = types.hdmf_common.VectorData( ...
'description', 'column #2', ...
'data', {'a';'b'} ...
);
 
my_table = types.hdmf_common.DynamicTable( ...
'description', 'an example table', ...
'colnames', {'col1', 'col2'}, ...
'col1', col1, ...
'col2', col2, ...
'id', types.hdmf_common.ElementIdentifiers('data', [0;1]) ... % 0-indexed, for compatibility with Python
);
my_table

MatNWB Setup

Start by setting up your MATLAB workspace. The code below adds the directory containing the MatNWB package to the MATLAB search path. MatNWB works by automatically creating API classes based on a defined schema.
%{
path_to_matnwb = '~/Repositories/matnwb'; % change to your own path location
addpath(genpath(pwd));
%}

Constructing a table with initialized columns

The DynamicTable class represents a column-based table to which you can add custom columns. It consists of a description, a list of columns , and a list of row IDs. You can create a DynamicTable by first defining the VectorData objects that will make up the columns of the table. Each VectorData object must contain the same number of rows. A list of rows IDs may be passed to the DynamicTable using the id argument. Row IDs are a useful way to access row information independent of row location index. The list of row IDs must be cast as an ElementIdentifiers object before being passed to the DynamicTable object. If no value is passed to id, an ElementIdentifiers object with 0-indexed row IDs will be created for you automatically.
MATLAB Syntax Note: Using column vectors is crucial to properly build vectors and tables. When defining individual values, make sure to use semi-colon (;) instead of instead of comma (,) when defining the data fields of these.
col1 = types.hdmf_common.VectorData( ...
'description', 'column #1', ...
'data', [1;2] ...
);
 
col2 = types.hdmf_common.VectorData( ...
'description', 'column #2', ...
'data', {'a';'b'} ...
);
 
my_table = types.hdmf_common.DynamicTable( ...
'description', 'an example table', ...
'colnames', {'col1', 'col2'}, ...
'col1', col1, ...
'col2', col2, ...
'id', types.hdmf_common.ElementIdentifiers('data', [0;1]) ... % 0-indexed, for compatibility with Python
);
my_table
my_table =
DynamicTable with properties: id: [1×1 types.hdmf_common.ElementIdentifiers] colnames: {'col1' 'col2'} description: 'an example table' vectordata: [2×1 types.untyped.Set] -

Adding rows

You can add rows to an existing DynamicTable using the object's addRow method. One way of using this method is to pass in the names of columns as parameter names followed by the elements to append. The class of the elements of the column must match the elements to append.
my_table.addRow('col1', 3, 'col2', {'c'}, 'id', 2);

Adding columns

You can add new columns to an existing DynamicTable object using the addColumn method. One way of using this method is to pass in the names of each new column followed by the corresponding values for each new column. The height of the new columns must match the height of the table.
col3 = types.hdmf_common.VectorData('description', 'column #3', ...
'data', [100; 200; 300]);
col4 = types.hdmf_common.VectorData('description', 'column #4', ...
'data', {'a1'; 'b2'; 'c3'});
 
my_table.addColumn('col3', col3,'col4', col4);

Create MATLAB table and convert to dynamic table

As an alternative to building a dynamic table using the DynamicTable and VectorData data types, it is also possible to create a MATLAB table and convert it to a dynamic table. Lets create the same table as before, but using MATLAB's table class:
% Create a table with two variables (columns):
T = table([1;2], {'a';'b'}, 'VariableNames', {'col1', 'col2'});
T.Properties.VariableDescriptions = {'column #1', 'column #2'};

Adding rows

T(end+1, :) = {3, 'c'};

Adding variables (columns)

T = addvars(T, [100;200;300], 'NewVariableNames',{'col3'});
T.Properties.VariableDescriptions{3} = 'column #3';
 
% Alternatively, a new variable can be added directly using dot syntax.
T.col4 = {'a1'; 'b2'; 'c3'};
T.Properties.VariableDescriptions{4} = 'column #4';
T
T = 3×4 table
 col1col2col3col4
11'a'100'a1'
22'b'200'b2'
33'c'300'c3'

Convert to dynamic table

dynamic_table = util.table2nwb(T, 'A MATLAB table that was converted to a dynamic table')
dynamic_table =
DynamicTable with properties: +

Adding rows

You can add rows to an existing DynamicTable using the object's addRow method. One way of using this method is to pass in the names of columns as parameter names followed by the elements to append. The class of the elements of the column must match the elements to append.
my_table.addRow('col1', 3, 'col2', {'c'}, 'id', 2);

Adding columns

You can add new columns to an existing DynamicTable object using the addColumn method. One way of using this method is to pass in the names of each new column followed by the corresponding values for each new column. The height of the new columns must match the height of the table.
col3 = types.hdmf_common.VectorData('description', 'column #3', ...
'data', [100; 200; 300]);
col4 = types.hdmf_common.VectorData('description', 'column #4', ...
'data', {'a1'; 'b2'; 'c3'});
 
my_table.addColumn('col3', col3);
my_table.addColumn('col4', col4);

Create MATLAB table and convert to dynamic table

As an alternative to building a dynamic table using the DynamicTable and VectorData data types, it is also possible to create a MATLAB table and convert it to a dynamic table. Lets create the same table as before, but using MATLAB's table class:
% Create a table with two variables (columns):
T = table([1;2], {'a';'b'}, 'VariableNames', {'col1', 'col2'});
T.Properties.VariableDescriptions = {'column #1', 'column #2'};

Adding rows

T(end+1, :) = {3, 'c'};

Adding variables (columns)

T = addvars(T, [100;200;300], 'NewVariableNames',{'col3'});
T.Properties.VariableDescriptions{3} = 'column #3';
 
% Alternatively, a new variable can be added directly using dot syntax.
T.col4 = {'a1'; 'b2'; 'c3'};
T.Properties.VariableDescriptions{4} = 'column #4';
T
T = 3×4 table
 col1col2col3col4
11'a'100'a1'
22'b'200'b2'
33'c'300'c3'

Convert to dynamic table

dynamic_table = util.table2nwb(T, 'A MATLAB table that was converted to a dynamic table')
dynamic_table =
DynamicTable with properties: id: [1×1 types.hdmf_common.ElementIdentifiers] colnames: {'col1' 'col2' 'col3' 'col4'} description: 'A MATLAB table that was converted to a dynamic table' vectordata: [4×1 types.untyped.Set] -

Enumerated (categorical) data

EnumData is a special type of column for storing an enumerated data type. This way each unique value is stored once, and the data references those values by index. Using this method is more efficient than storing a single value many times, and has the advantage of communicating to downstream tools that the data is categorical in nature.

Warning Regarding EnumData

EnumData is currently an experimental feature and as such should not be used in a production environment.
CellTypeElements = types.hdmf_common.VectorData(...
'description', 'fixed set of elements referenced by cell_type' ...
, 'data', {'aa', 'bb', 'cc'} ... % the enumerated elements
);
CellType = types.hdmf_experimental.EnumData( ...
'description', 'this column holds categorical variables' ... % properties derived from VectorData
, 'data', [0, 1, 2, 1, 0] ... % zero-indexed offset to elements.
, 'elements', types.untyped.ObjectView(CellTypeElements) ...
);
 
MyTable = types.hdmf_common.DynamicTable('description', 'an example table');
MyTable.vectordata.set('cell_type_elements', CellTypeElements); % the *_elements format is required for compatibility with pynwb
MyTable.addColumn('cell_type', CellType);

Ragged array columns

A table column with a different number of elements for each row is called a "ragged array column." To define a table with a ragged array column, pass both the VectorData and the corresponding VectorIndex as columns of the DynamicTable object. The VectorData columns will contain the data values. The VectorIndex column serves to indicate how to arrange the data across rows. By convention the VectorIndex object corresponding to a particular column must have have the same name with the addition of the '_index' suffix.
Below, the VectorIndex values indicate to place the 1st to 3rd (inclusive) elements of the VectorData into the first row and 4th element into the second row. The resulting table will have the cell {'1a'; '1b'; '1c'} in the first row and the cell {'2a'} in the second row.
 
col1 = types.hdmf_common.VectorData( ...
'description', 'column #1', ...
'data', {'1a'; '1b'; '1c'; '2a'} ...
);
 
col1_index = types.hdmf_common.VectorIndex( ...
'description', 'column #1 index', ...
'target',types.untyped.ObjectView(col1), ... % object view of target column
'data', [3; 4] ...
);
 
table_ragged_col = types.hdmf_common.DynamicTable( ...
'description', 'an example table', ...
'colnames', {'col1'}, ...
'col1', col1, ...
'col1_index', col1_index, ...
'id', types.hdmf_common.ElementIdentifiers('data', [0; 1]) ... % 0-indexed, for compatibility with Python
);

Adding ragged array rows

You can add a new row to the ragged array column. Under the hood, the addRow method will add the appropriate value to the VectorIndex column to maintain proper formatting.
table_ragged_col.addRow('col1', {'3a'; '3b'; '3c'}, 'id', 2);

Accessing row elements

You can access data from entire rows of a DynamicTable object by calling the getRow method for the corresponding object. You can supply either an individual row number or a list of row numbers.
my_table.getRow(1)
ans = 1×4 table
 col1col2col3col4
11'a'100'a1'
If you want to access values for just a subset of columns you can pass in the 'columns' arguement along with a cell array with the desired column names
my_table.getRow(1:3, 'columns', {'col1'})
ans = 3×1 table
 col1
11
22
33
You can also access specific rows by their corresponding row ID's, if they have been defined, by supplying a 'true' Boolean to the 'useId' parameter
my_table.getRow(1, 'useId', true)
ans = 1×4 table
 col1col2col3col4
12'b'200'b2'
For a ragged array columns, the getRow method will return a cell with different number of elements for each row
table_ragged_col.getRow(1:2)
ans = 2×1 table
 col1
1[{'1a'};{'1b'};{'1c'}]
21×1 cell

Accessing column elements

To acess all rows from a particular column use the .get method on the vectordata field of the DynamicTable object
 
my_table.vectordata.get('col2').data
ans = 3×1 cell
'a'
'b'
'c'

Referencing rows of other tables

You can create a column that references rows of other tables by adding a DynamicTableRegion object as a column of a DynamicTable. This is analogous to a foreign key in a relational database. The DynamicTableRegion class takes in an ObjectView object as arguement. ObjectView objects create links from one object type referencing another.
dtr_col = types.hdmf_common.DynamicTableRegion( ...
'description', 'references multiple rows of earlier table', ...
'data', [0; 1; 1; 0], ... # 0-indexed
'table',types.untyped.ObjectView(my_table) ... % object view of target table
);
 
data_col = types.hdmf_common.VectorData( ...
'description', 'data column', ...
'data', {'a'; 'b'; 'c'; 'd'} ...
);
 
dtr_table = types.hdmf_common.DynamicTable( ...
'description', 'test table with DynamicTableRegion', ...
'colnames', {'data_col', 'dtr_col'}, ...
'dtr_col', dtr_col, ...
'data_col',data_col, ...
'id',types.hdmf_common.ElementIdentifiers('data', [0; 1; 2; 3]) ...
);

Converting a DynamicTable to a MATLAB table

You can convert a DynamicTable object to a MATLAB table by making use of the object's toTable method. This is a useful way to view the whole table in a human-readable format.
my_table.toTable()
ans = 3×5 table
 idcol1col2col3col4
101'a'100'a1'
212'b'200'b2'
323'c'300'c3'
When the DynamicTable object contains a column that references other tables, you can pass in a Boolean to indicate whether to include just the row indices of the referenced table. Passing in false will result in inclusion of the referenced rows as nested tables.
dtr_table.toTable(false)
ans = 4×3 table
 iddata_coldtr_col
10'a'1×4 table
21'b'1×4 table
32'c'1×4 table
43'd'1×4 table

Creating an expandable table

When using the default HDF5 backend, each column of these tables is an HDF5 Dataset, which by default are set to an unchangeable size. This means that once a file is written, it is not possible to add a new row. If you want to be able to save this file, load it, and add more rows to the table, you will need to set this up when you create the VectorData and ElementIdentifiers columns of a DynamicTable. Specifically, you must wrap the column data with a DataPipe object. The DataPipe class takes in maxSize and axis as arguments to indicate the maximum desired size for each axis and the axis to whcih to append to, respectively. For example, creating a DataPipe object with a maxSize value equal to [Inf, 1] indicates that the number of rows may increase indifinetely. In contrast, setting maxSize equal to [8, 1] would allow the column to grow to a maximum height of 8.
% create NwbFile object with required fields
file= NwbFile( ...
'session_start_time', datetime('2021-01-01 00:00:00', 'TimeZone', 'local'), ...
'identifier', 'ident1', ...
'session_description', 'ExpandableTableTutorial' ...
);
 
% create VectorData objects with DataPipe objects
start_time_exp = types.hdmf_common.VectorData( ...
'description', 'start times column', ...
'data', types.untyped.DataPipe( ...
'data', [1, 2], ... # data must be numerical
'maxSize', Inf ...
) ...
);
 
stop_time_exp = types.hdmf_common.VectorData( ...
'description', 'stop times column', ...
'data', types.untyped.DataPipe( ...
'data', [2, 3], ... #data must be numerical
'maxSize', Inf ...
) ...
);
 
random_exp = types.hdmf_common.VectorData( ...
'description', 'random data column', ...
'data', types.untyped.DataPipe( ...
'data', rand(5, 2), ... #data must be numerical
'maxSize', [5, Inf], ...
'axis', 2 ...
) ...
);
 
ids_exp = types.hdmf_common.ElementIdentifiers( ...
'data', types.untyped.DataPipe( ...
'data', int32([0; 1]), ... # data must be numerical
'maxSize', Inf ...
) ...
);
% create expandable table
colnames = {'start_time', 'stop_time', 'randomvalues'};
file.intervals_trials = types.core.TimeIntervals( ...
'description', 'test expdandable dynamic table', ...
'colnames', colnames, ...
'start_time', start_time_exp, ...
'stop_time', stop_time_exp, ...
'randomvalues', random_exp, ...
'id', ids_exp ...
);
% export file
nwbExport(file, 'expandableTableTestFile.nwb');
Now, you can read in the file, add more rows, and save again to file
readFile = nwbRead('expandableTableTestFile.nwb', 'ignorecache');
readFile.intervals_trials.addRow( ...
'start_time', 3, ...
'stop_time', 4, ...
'randomvalues', rand(5,1), ...
'id', 2 ...
)
nwbExport(readFile, 'expandableTableTestFile.nwb');
Note: DataPipe objects change how the dimension of the datasets for each column map onto the shape of HDF5 datasets. See README for more details.

Multidimensional Columns

The order of dimensions of multidimensional columns in MatNWB is reversed relative to the Python HDMF package (see README for detailed explanation). Therefore, the height of a multidimensional column belonging to a DynamicTable object is defined by the shape of its last dimension. A valid DynamicTable must have matched height across columns.

Constructing multidimensional columns

% Define 1D column
simple_col = types.hdmf_common.VectorData( ...
'description', '1D column',...
'data', rand(10,1) ...
);
% Define ND column
multi_col = types.hdmf_common.VectorData( ...
'description', 'multidimensional column',...
'data', rand(3,2,10) ...
);
% construct table
multi_dim_table = types.hdmf_common.DynamicTable( ...
'description','test table', ...
'colnames', {'simple','multi'}, ...
'simple', simple_col, ...
'multi', multi_col, ...
'id', types.hdmf_common.ElementIdentifiers('data', (0:9)') ... % 0-indexed, for compatibility with Python
);
 

Multidimensional ragged array columns

DynamicTable objects with multidimensional ragged array columns can be constructed by passing in the corresponding VectorIndex column
% Define column with data
multi_ragged_col = types.hdmf_common.VectorData( ...
'description', 'multidimensional ragged array column',...
'data', rand(2,3,5) ...
);
% Define column with VectorIndex
multi_ragged_index = types.hdmf_common.VectorIndex( ...
'description', 'index to multi_ragged_col', ...
'target', types.untyped.ObjectView(multi_ragged_col),'data', [2; 3; 5] ...
);
 
multi_ragged_table = types.hdmf_common.DynamicTable( ...
'description','test table', ...
'colnames', {'multi_ragged'}, ...
'multi_ragged', multi_ragged_col, ...
'multi_ragged_index', multi_ragged_index, ...
'id', types.hdmf_common.ElementIdentifiers('data', [0; 1; 2]) ... % 0-indexed, for compatibility with Python
);

Adding rows to multidimensional array columns

DynamicTable objects with multidimensional array columns can also be constructed by adding a single row at a time. This method makes use of DataPipe objects due to the fact that MATLAB doesn't support singleton dimensions for arrays with more than 2 dimensions. The code block below demonstates how to build a DynamicTable object with a mutidimensional raaged array column in this manner.
% Create file
file = NwbFile( ...
'session_start_time', datetime('2021-01-01 00:00:00', 'TimeZone', 'local'), ...
'identifier', 'ident1', ...
'session_description', 'test_file' ...
);
 
% Define Vector Data Objects with first row of table
start_time_exp = types.hdmf_common.VectorData( ...
'description', 'start times column', ...
'data', types.untyped.DataPipe( ...
'data', 1, ...
'maxSize', Inf ...
) ...
);
stop_time_exp = types.hdmf_common.VectorData( ...
'description', 'stop times column', ...
'data', types.untyped.DataPipe( ...
'data', 10, ...
'maxSize', Inf ...
) ...
);
random_exp = types.hdmf_common.VectorData( ...
'description', 'random data column', ...
'data', types.untyped.DataPipe( ...
'data', rand(3,2,5), ... #random data
'maxSize', [3, 2, Inf], ...
'axis', 3 ...
) ...
);
random_exp_index = types.hdmf_common.VectorIndex( ...
'description', 'index to random data column', ...
'target',types.untyped.ObjectView(random_exp), ...
'data', types.untyped.DataPipe( ...
'data', uint64(5), ...
'maxSize', Inf ...
) ...
);
ids_exp = types.hdmf_common.ElementIdentifiers( ...
'data', types.untyped.DataPipe( ...
'data', int64(0), ... # data must be numerical
'maxSize', Inf ...
) ...
);
% Create expandable table
colnames = {'start_time', 'stop_time', 'randomvalues'};
file.intervals_trials = types.core.TimeIntervals( ...
'description', 'test expdandable dynamic table', ...
'colnames', colnames, ...
'start_time', start_time_exp, ...
'stop_time', stop_time_exp, ...
'randomvalues', random_exp, ...
'randomvalues_index', random_exp_index, ...
'id', ids_exp ...
);
% Export file
nwbExport(file, 'multiRaggedExpandableTableTest.nwb');
% Read in file
read_file = nwbRead('multiRaggedExpandableTableTest.nwb', 'ignorecache');
% add individual rows
read_file.intervals_trials.addRow( ...
'start_time', 2, ...
'stop_time', 20, ...
'randomvalues', rand(3,2,6), ...
'id', 1 ...
);
read_file.intervals_trials.addRow( ...
'start_time', 3, ...
'stop_time', 30, ...
'randomvalues', rand(3,2,3), ...
'id', 2 ...
);
read_file.intervals_trials.addRow( ...
'start_time', 4, ...
'stop_time', 40, ...
'randomvalues', rand(3,2,8), ...
'id', 3 ...
);
 

Learn More!

Python Tutorial

+

Enumerated (categorical) data

EnumData is a special type of column for storing an enumerated data type. This way each unique value is stored once, and the data references those values by index. Using this method is more efficient than storing a single value many times, and has the advantage of communicating to downstream tools that the data is categorical in nature.

Warning Regarding EnumData

EnumData is currently an experimental feature and as such should not be used in a production environment.
CellTypeElements = types.hdmf_common.VectorData(...
'description', 'fixed set of elements referenced by cell_type' ...
, 'data', {'aa', 'bb', 'cc'} ... % the enumerated elements
);
CellType = types.hdmf_experimental.EnumData( ...
'description', 'this column holds categorical variables' ... % properties derived from VectorData
, 'data', [0, 1, 2, 1, 0] ... % zero-indexed offset to elements.
, 'elements', types.untyped.ObjectView(CellTypeElements) ...
);
 
MyTable = types.hdmf_common.DynamicTable('description', 'an example table');
MyTable.vectordata.set('cell_type_elements', CellTypeElements); % the *_elements format is required for compatibility with pynwb
MyTable.addColumn('cell_type', CellType);

Ragged array columns

A table column with a different number of elements for each row is called a "ragged array column." To define a table with a ragged array column, pass both the VectorData and the corresponding VectorIndex as columns of the DynamicTable object. The VectorData columns will contain the data values. The VectorIndex column serves to indicate how to arrange the data across rows. By convention the VectorIndex object corresponding to a particular column must have have the same name with the addition of the '_index' suffix.
Below, the VectorIndex values indicate to place the 1st to 3rd (inclusive) elements of the VectorData into the first row and 4th element into the second row. The resulting table will have the cell {'1a'; '1b'; '1c'} in the first row and the cell {'2a'} in the second row.
 
col1 = types.hdmf_common.VectorData( ...
'description', 'column #1', ...
'data', {'1a'; '1b'; '1c'; '2a'} ...
);
 
col1_index = types.hdmf_common.VectorIndex( ...
'description', 'column #1 index', ...
'target',types.untyped.ObjectView(col1), ... % object view of target column
'data', [3; 4] ...
);
 
table_ragged_col = types.hdmf_common.DynamicTable( ...
'description', 'an example table', ...
'colnames', {'col1'}, ...
'col1', col1, ...
'col1_index', col1_index, ...
'id', types.hdmf_common.ElementIdentifiers('data', [0; 1]) ... % 0-indexed, for compatibility with Python
);

Adding ragged array rows

You can add a new row to the ragged array column. Under the hood, the addRow method will add the appropriate value to the VectorIndex column to maintain proper formatting.
table_ragged_col.addRow('col1', {'3a'; '3b'; '3c'}, 'id', 2);

Accessing row elements

You can access data from entire rows of a DynamicTable object by calling the getRow method for the corresponding object. You can supply either an individual row number or a list of row numbers.
my_table.getRow(1)
ans = 1×4 table
 col1col2col3col4
11'a'100'a1'
If you want to access values for just a subset of columns you can pass in the 'columns' arguement along with a cell array with the desired column names
my_table.getRow(1:3, 'columns', {'col1'})
ans = 3×1 table
 col1
11
22
33
You can also access specific rows by their corresponding row ID's, if they have been defined, by supplying a 'true' Boolean to the 'useId' parameter
my_table.getRow(1, 'useId', true)
ans = 1×4 table
 col1col2col3col4
12'b'200'b2'
For a ragged array columns, the getRow method will return a cell with different number of elements for each row
table_ragged_col.getRow(1:2)
ans = 2×1 table
 col1
13×1 cell
21×1 cell

Accessing column elements

To acess all rows from a particular column use the .get method on the vectordata field of the DynamicTable object
 
my_table.vectordata.get('col2').data
ans = 3×1 cell
'a'
'b'
'c'

Referencing rows of other tables

You can create a column that references rows of other tables by adding a DynamicTableRegion object as a column of a DynamicTable. This is analogous to a foreign key in a relational database. The DynamicTableRegion class takes in an ObjectView object as arguement. ObjectView objects create links from one object type referencing another.
dtr_col = types.hdmf_common.DynamicTableRegion( ...
'description', 'references multiple rows of earlier table', ...
'data', [0; 1; 1; 0], ... # 0-indexed
'table',types.untyped.ObjectView(my_table) ... % object view of target table
);
 
data_col = types.hdmf_common.VectorData( ...
'description', 'data column', ...
'data', {'a'; 'b'; 'c'; 'd'} ...
);
 
dtr_table = types.hdmf_common.DynamicTable( ...
'description', 'test table with DynamicTableRegion', ...
'colnames', {'dtr_col','data_col'}, ...
'dtr_col', dtr_col, ...
'data_col',data_col, ...
'id',types.hdmf_common.ElementIdentifiers('data', [0; 1; 2; 3]) ...
);

Converting a DynamicTable to a MATLAB table

You can convert a DynamicTable object to a MATLAB table by making use of the object's toTable method. This is a useful way to view the whole table in a human-readable format.
my_table.toTable()
ans = 3×5 table
 idcol1col2col3col4
101'a'100'a1'
212'b'200'b2'
323'c'300'c3'
When the DynamicTable object contains a column that references other tables, you can pass in a Boolean to indicate whether to include just the row indices of the referenced table. Passing in false will result in inclusion of the referenced rows as nested tables.
dtr_table.toTable(false)
ans = 4×3 table
 iddtr_coldata_col
101×4 table'a'
211×4 table'b'
321×4 table'c'
431×4 table'd'

Creating an expandable table

When using the default HDF5 backend, each column of these tables is an HDF5 Dataset, which by default are set to an unchangeable size. This means that once a file is written, it is not possible to add a new row. If you want to be able to save this file, load it, and add more rows to the table, you will need to set this up when you create the VectorData and ElementIdentifiers columns of a DynamicTable. Specifically, you must wrap the column data with a DataPipe object. The DataPipe class takes in maxSize and axis as arguments to indicate the maximum desired size for each axis and the axis to whcih to append to, respectively. For example, creating a DataPipe object with a maxSize value equal to [Inf, 1] indicates that the number of rows may increase indifinetely. In contrast, setting maxSize equal to [8, 1] would allow the column to grow to a maximum height of 8.
% create NwbFile object with required fields
file = NwbFile( ...
'session_start_time', datetime('2021-01-01 00:00:00', 'TimeZone', 'local'), ...
'identifier', 'ident1', ...
'session_description', 'ExpandableTableTutorial' ...
);
 
% create VectorData objects with DataPipe objects
start_time_exp = types.hdmf_common.VectorData( ...
'description', 'start times column', ...
'data', types.untyped.DataPipe( ...
'data', [1, 2], ... # data must be numerical
'maxSize', Inf ...
) ...
);
 
stop_time_exp = types.hdmf_common.VectorData( ...
'description', 'stop times column', ...
'data', types.untyped.DataPipe( ...
'data', [2, 3], ... #data must be numerical
'maxSize', Inf ...
) ...
);
 
random_exp = types.hdmf_common.VectorData( ...
'description', 'random data column', ...
'data', types.untyped.DataPipe( ...
'data', rand(5, 2), ... #data must be numerical
'maxSize', [5, Inf], ...
'axis', 2 ...
) ...
);
 
ids_exp = types.hdmf_common.ElementIdentifiers( ...
'data', types.untyped.DataPipe( ...
'data', int32([0; 1]), ... # data must be numerical
'maxSize', Inf ...
) ...
);
% create expandable table
colnames = {'start_time', 'stop_time', 'randomvalues'};
file.intervals_trials = types.core.TimeIntervals( ...
'description', 'test expdandable dynamic table', ...
'colnames', colnames, ...
'start_time', start_time_exp, ...
'stop_time', stop_time_exp, ...
'randomvalues', random_exp, ...
'id', ids_exp ...
);
% export file
nwbExport(file, 'expandableTableTestFile.nwb');
Now, you can read in the file, add more rows, and save again to file
readFile = nwbRead('expandableTableTestFile.nwb', 'ignorecache');
readFile.intervals_trials.addRow( ...
'start_time', 3, ...
'stop_time', 4, ...
'randomvalues', rand(5,1), ...
'id', 2 ...
)
nwbExport(readFile, 'expandableTableTestFile.nwb');
Note: DataPipe objects change how the dimension of the datasets for each column map onto the shape of HDF5 datasets. See README for more details.

Multidimensional Columns

The order of dimensions of multidimensional columns in MatNWB is reversed relative to the Python HDMF package (see README for detailed explanation). Therefore, the height of a multidimensional column belonging to a DynamicTable object is defined by the shape of its last dimension. A valid DynamicTable must have matched height across columns.

Constructing multidimensional columns

% Define 1D column
simple_col = types.hdmf_common.VectorData( ...
'description', '1D column',...
'data', rand(10,1) ...
);
% Define ND column
multi_col = types.hdmf_common.VectorData( ...
'description', 'multidimensional column',...
'data', rand(3,2,10) ...
);
% construct table
multi_dim_table = types.hdmf_common.DynamicTable( ...
'description','test table', ...
'colnames', {'simple','multi'}, ...
'simple', simple_col, ...
'multi', multi_col, ...
'id', types.hdmf_common.ElementIdentifiers('data', (0:9)') ... % 0-indexed, for compatibility with Python
);
 

Multidimensional ragged array columns

DynamicTable objects with multidimensional ragged array columns can be constructed by passing in the corresponding VectorIndex column
% Define column with data
multi_ragged_col = types.hdmf_common.VectorData( ...
'description', 'multidimensional ragged array column',...
'data', rand(2,3,5) ...
);
% Define column with VectorIndex
multi_ragged_index = types.hdmf_common.VectorIndex( ...
'description', 'index to multi_ragged_col', ...
'target', types.untyped.ObjectView(multi_ragged_col),'data', [2; 3; 5] ...
);
 
multi_ragged_table = types.hdmf_common.DynamicTable( ...
'description','test table', ...
'colnames', {'multi_ragged'}, ...
'multi_ragged', multi_ragged_col, ...
'multi_ragged_index', multi_ragged_index, ...
'id', types.hdmf_common.ElementIdentifiers('data', [0; 1; 2]) ... % 0-indexed, for compatibility with Python
);

Adding rows to multidimensional array columns

DynamicTable objects with multidimensional array columns can also be constructed by adding a single row at a time. This method makes use of DataPipe objects due to the fact that MATLAB doesn't support singleton dimensions for arrays with more than 2 dimensions. The code block below demonstates how to build a DynamicTable object with a mutidimensional raaged array column in this manner.
% Create file
file = NwbFile( ...
'session_start_time', datetime('2021-01-01 00:00:00', 'TimeZone', 'local'), ...
'identifier', 'ident1', ...
'session_description', 'test_file' ...
);
 
% Define Vector Data Objects with first row of table
start_time_exp = types.hdmf_common.VectorData( ...
'description', 'start times column', ...
'data', types.untyped.DataPipe( ...
'data', 1, ...
'maxSize', Inf ...
) ...
);
stop_time_exp = types.hdmf_common.VectorData( ...
'description', 'stop times column', ...
'data', types.untyped.DataPipe( ...
'data', 10, ...
'maxSize', Inf ...
) ...
);
random_exp = types.hdmf_common.VectorData( ...
'description', 'random data column', ...
'data', types.untyped.DataPipe( ...
'data', rand(3,2,5), ... #random data
'maxSize', [3, 2, Inf], ...
'axis', 3 ...
) ...
);
random_exp_index = types.hdmf_common.VectorIndex( ...
'description', 'index to random data column', ...
'target',types.untyped.ObjectView(random_exp), ...
'data', types.untyped.DataPipe( ...
'data', uint64(5), ...
'maxSize', Inf ...
) ...
);
ids_exp = types.hdmf_common.ElementIdentifiers( ...
'data', types.untyped.DataPipe( ...
'data', int64(0), ... # data must be numerical
'maxSize', Inf ...
) ...
);
% Create expandable table
colnames = {'start_time', 'stop_time', 'randomvalues'};
file.intervals_trials = types.core.TimeIntervals( ...
'description', 'test expdandable dynamic table', ...
'colnames', colnames, ...
'start_time', start_time_exp, ...
'stop_time', stop_time_exp, ...
'randomvalues', random_exp, ...
'randomvalues_index', random_exp_index, ...
'id', ids_exp ...
);
% Export file
nwbExport(file, 'multiRaggedExpandableTableTest.nwb');
% Read in file
read_file = nwbRead('multiRaggedExpandableTableTest.nwb', 'ignorecache');
% add individual rows
read_file.intervals_trials.addRow( ...
'start_time', 2, ...
'stop_time', 20, ...
'randomvalues', rand(3,2,6), ...
'id', 1 ...
);
read_file.intervals_trials.addRow( ...
'start_time', 3, ...
'stop_time', 30, ...
'randomvalues', rand(3,2,3), ...
'id', 2 ...
);
read_file.intervals_trials.addRow( ...
'start_time', 4, ...
'stop_time', 40, ...
'randomvalues', rand(3,2,8), ...
'id', 3 ...
);
 

Learn More!

Python Tutorial