Skip to content

Commit

Permalink
Merge pull request #164 from NeurodataWithoutBorders/todo/file-amend
Browse files Browse the repository at this point in the history
Allow Amending NWB Files
  • Loading branch information
lawrence-mbf authored Oct 4, 2019
2 parents 67ba0f2 + f1eef41 commit 3353784
Show file tree
Hide file tree
Showing 14 changed files with 353 additions and 187 deletions.
20 changes: 14 additions & 6 deletions +file/fillExport.m
Original file line number Diff line number Diff line change
Expand Up @@ -140,17 +140,25 @@
% obj, loc_id, path, refs
fde = ['refs = obj.' name '.export(fid, ' fullpath ', refs);'];
elseif isa(prop, 'file.Dataset') %untyped dataset
if prop.scalar
forceArrayFlag = '';
else
forceArrayFlag = ', ''forceArray''';
options = {};
if ~prop.scalar
options = [options {'''forceArray'''}];
end

% special case due to unique behavior of file_create_date
if strcmp(name, 'file_create_date')
options = [options {'''forceChunking'''}];
end
% just to guarantee optional arguments are correct syntax
nameProp = sprintf('obj.%s', name);
nameArgs = [{nameProp} options];
nameArgs = strjoin(nameArgs, ', ');
fde = strjoin({...
['if startsWith(class(obj.' name '), ''types.untyped.'')']...
[' refs = obj.' name '.export(fid, ' fullpath ', refs);']...
['elseif ~isempty(obj.' name ')']...
[' ' sprintf('io.writeDataset(fid, %1$s, obj.%2$s%3$s);',...
fullpath, name, forceArrayFlag)]...
[' ' sprintf('io.writeDataset(fid, %1$s, %2$s);',...
fullpath, nameArgs)]...
'end'...
}, newline);
else
Expand Down
29 changes: 20 additions & 9 deletions +io/mapData2H5.m
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,44 @@
% Given base file_id, type string and data value, returns HDF5 type id, space id,
% and properly converted data

forceArray = false;
if ~isempty(varargin)
assert(iscellstr(varargin), 'options must be character arrays.');
forceArray = any(strcmp(varargin, 'forceArray'));
end
forceArray = any(strcmp('forceArray', varargin));
forceChunked = any(strcmp('forceChunking', varargin));

tid = io.getBaseType(class(data));

% max size is always unlimited
unlimited_size = H5ML.get_constant_value('H5S_UNLIMITED');
%determine space size
if ischar(data)
if ~forceArray && size(data,1) == 1
sid = H5S.create('H5S_SCALAR');
else
sid = H5S.create_simple(1, size(data,1), []);
dims = size(data, 1);
if forceChunked
max_dims = repmat(unlimited_size, size(dims));
else
max_dims = [];
end
sid = H5S.create_simple(1, size(data,1), max_dims);
end
elseif ~forceArray && isscalar(data)
sid = H5S.create('H5S_SCALAR');
else
if isvector(data)
nd = 1;
num_dims = 1;
dims = length(data);
else
nd = ndims(data);
num_dims = ndims(data);
dims = size(data);
end

sid = H5S.create_simple(nd, fliplr(dims), []);
dims = fliplr(dims);
if forceChunked
max_dims = repmat(unlimited_size, size(dims));
else
max_dims = [];
end
sid = H5S.create_simple(num_dims, dims, max_dims);
end

%% Do Data Conversions
Expand Down
36 changes: 35 additions & 1 deletion +io/writeDataset.m
Original file line number Diff line number Diff line change
@@ -1,6 +1,40 @@
function writeDataset(fid, fullpath, data, varargin)
assert(isempty(varargin) || iscellstr(varargin),...
'options should be character arrays.');
[tid, sid, data] = io.mapData2H5(fid, data, varargin{:});
did = H5D.create(fid, fullpath, tid, sid, 'H5P_DEFAULT');
[~, dims, ~] = H5S.get_simple_extent_dims(sid);
try
dcpl = H5P.create('H5P_DATASET_CREATE');
if any(strcmp('forceChunking', varargin))
H5P.set_chunk(dcpl, dims)
end
did = H5D.create(fid, fullpath, tid, sid, dcpl);
H5P.close(dcpl);
catch ME
if contains(ME.message, 'name already exists')
did = H5D.open(fid, fullpath);
create_plist = H5D.get_create_plist(did);
edit_sid = H5D.get_space(did);
[~, edit_dims, ~] = H5S.get_simple_extent_dims(edit_sid);
layout = H5P.get_layout(create_plist);
is_chunked = layout == H5ML.get_constant_value('H5D_CHUNKED');
is_same_dims = all(edit_dims == dims);
if ~is_same_dims && is_chunked
H5D.set_extent(did, dims);
elseif ~is_same_dims
warning('Attempted to change size of continuous dataset `%s`. Skipping.',...
fullpath);
end
H5P.close(create_plist);
H5S.close(edit_sid);
else
rethrow(ME);
end
end
H5D.write(did, tid, sid, sid, 'H5P_DEFAULT', data);
H5D.close(did);
if isa(tid, 'H5ML.id')
H5T.close(tid);
end
H5S.close(sid);
end
4 changes: 3 additions & 1 deletion +io/writeGroup.m
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
function writeGroup(fid, fullpath)
function groupExists = writeGroup(fid, fullpath)
groupExists = false;
defaultProplist = 'H5P_DEFAULT';

% validate path
Expand All @@ -24,6 +25,7 @@ function writeGroup(fid, fullpath)
groupId = H5G.open(fid, path, defaultProplist);
if strcmp(path, fullpath) % fullpath already exists
H5G.close(groupId);
groupExists = true;
return;
end
deepestGroup = groupId;
Expand Down
20 changes: 20 additions & 0 deletions +tests/+system/AmendTest.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
classdef AmendTest < tests.system.NwbTestInterface
methods (Test)
function testAmend(testCase)
filename = ['MatNWB.' testCase.className() '.testRoundTrip.nwb'];
nwbExport(testCase.file, filename);
testCase.appendContainer(testCase.file);
nwbExport(testCase.file, filename);

writeContainer = testCase.getContainer(testCase.file);
readFile = nwbRead(filename);
readContainer = testCase.getContainer(readFile);
testCase.verifyContainerEqual(readContainer, writeContainer);
end
end

methods (Abstract)
appendContainer(testCase, file);
end
end

12 changes: 11 additions & 1 deletion +tests/+system/DynamicTableTest.m
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
classdef DynamicTableTest < tests.system.RoundTripTest
classdef DynamicTableTest < tests.system.RoundTripTest & tests.system.AmendTest
methods
function addContainer(~, file)
start_time = types.core.VectorData(...
Expand Down Expand Up @@ -31,6 +31,16 @@ function addContainer(~, file)
function c = getContainer(~, file)
c = file.intervals_trials.vectordata.get('randomvalues');
end

function appendContainer(testCase, file)
container = testCase.getContainer(file);
container.data = rand(500, 1); % new random values.
file.intervals_trials.colnames{end+1} = 'newcolumn';
file.intervals_trials.vectordata.set('newcolumn',...
types.core.VectorData(...
'description', 'newly added column',...
'data', 100:-1:1));
end
end
end

44 changes: 22 additions & 22 deletions +tests/+system/NWBFileIOTest.m
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
classdef NWBFileIOTest < tests.system.PyNWBIOTest
methods
function addContainer(testCase, file) %#ok<INUSL>
ts = types.core.TimeSeries(...
'data', int32(100:10:190) .', ...
'data_unit', 'SIunit', ...
'timestamps', (0:9) .', ...
'data_resolution', 0.1);
file.acquisition.set('test_timeseries', ts);
clust = types.core.Clustering( ...
'description', 'A fake Clustering interface', ...
'num', [0, 1, 2, 0, 1, 2] .', ...
'peak_over_rms', [100, 101, 102] .', ...
'times', (10:10:60) .');
mod = types.core.ProcessingModule( ...
'description', 'a test module', ...
'Clustering', clust);
file.processing.set('test_module', mod);
methods
function addContainer(testCase, file) %#ok<INUSL>
ts = types.core.TimeSeries(...
'data', int32(100:10:190) .', ...
'data_unit', 'SIunit', ...
'timestamps', (0:9) .', ...
'data_resolution', 0.1);
file.acquisition.set('test_timeseries', ts);
clust = types.core.Clustering( ...
'description', 'A fake Clustering interface', ...
'num', [0, 1, 2, 0, 1, 2] .', ...
'peak_over_rms', [100, 101, 102] .', ...
'times', (10:10:60) .');
mod = types.core.ProcessingModule( ...
'description', 'a test module', ...
'Clustering', clust);
file.processing.set('test_module', mod);
end

function c = getContainer(testCase, file) %#ok<INUSL>
c = file;
end
end

function c = getContainer(testCase, file) %#ok<INUSL>
c = file;
end
end
end

104 changes: 104 additions & 0 deletions +tests/+system/NwbTestInterface.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
classdef NwbTestInterface < matlab.unittest.TestCase
properties
% registry
file
root;
end

methods (TestClassSetup)
function setupClass(testCase)
rootPath = fullfile(fileparts(mfilename('fullpath')), '..', '..');
testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath));
testCase.root = rootPath;
end
end

methods (TestMethodSetup)
function setupMethod(testCase)
testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture);
generateCore(fullfile(testCase.root, ...
'schema', 'core', 'nwb.namespace.yaml'));
testCase.file = NwbFile( ...
'session_description', 'a test NWB File', ...
'identifier', 'TEST123', ...
'session_start_time', '2018-12-02T12:57:27.371444-08:00', ...
'file_create_date', datestr([2017, 4, 15, 12, 0, 0], 'yyyy-mm-dd HH:MM:SS'),...
'timestamps_reference_time', '2018-12-02T12:57:27.371444-08:00');
testCase.addContainer(testCase.file);
end
end

methods
function n = className(testCase)
classSplit = strsplit(class(testCase), '.');
n = classSplit{end};
end

function verifyContainerEqual(testCase, actual, expected)
testCase.verifyEqual(class(actual), class(expected));
props = properties(actual);
for i = 1:numel(props)
prop = props{i};
if strcmp(prop, 'file_create_date')
continue;
end
val1 = actual.(prop);
val2 = expected.(prop);
failmsg = ['Values for property ''' prop ''' are not equal'];
if startsWith(class(val1), 'types.core.')
verifyContainerEqual(testCase, val1, val2);
elseif isa(val1, 'types.untyped.Set')
verifySetEqual(testCase, val1, val2, failmsg);
elseif isdatetime(val1)
testCase.verifyEqual(char(val1), char(val2));
else
if isa(val1, 'types.untyped.DataStub')
trueval = val1.load();
else
trueval = val1;
end

if isvector(val2) && isvector(trueval) && numel(val2) == numel(trueval)
trueval = reshape(trueval, size(val2));
end
testCase.verifyEqual(trueval, val2, failmsg);
end
end
end

function verifySetEqual(testCase, actual, expected, failmsg)
testCase.verifyEqual(class(actual), class(expected));
ak = actual.keys();
ek = expected.keys();
verifyTrue(testCase, isempty(setxor(ak, ek)), failmsg);
for i=1:numel(ak)
key = ak{i};
verifyContainerEqual(testCase, actual.get(key), ...
expected.get(key));
end
end

function verifyUntypedEqual(testCase, actual, expected)
testCase.verifyEqual(class(actual), class(expected));
props = properties(actual);
for i = 1:numel(props)
prop = props{i};
val1 = actual.(prop);
val2 = expected.(prop);
if isa(val1, 'types.core.NWBContainer') || isa(val1, 'types.core.NWBData')
verifyContainerEqual(testCase, val1, val2);
else
testCase.verifyEqual(val1, val2, ...
['Values for property ''' prop ''' are not equal']);
end
end
end

end

methods(Abstract)
addContainer(testCase, file);
c = getContainer(testCase, file);
end
end

19 changes: 18 additions & 1 deletion +tests/+system/PhotonSeriesIOTest.m
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
classdef PhotonSeriesIOTest < tests.system.PyNWBIOTest
classdef PhotonSeriesIOTest < tests.system.PyNWBIOTest & tests.system.AmendTest
methods
function addContainer(testCase, file) %#ok<INUSL>
dev = types.core.Device();
Expand Down Expand Up @@ -34,6 +34,23 @@ function addContainer(testCase, file) %#ok<INUSL>
function c = getContainer(testCase, file) %#ok<INUSL>
c = file.acquisition.get('test_2ps');
end

function appendContainer(~, file)
oldImagingPlane = file.general_optophysiology.get('imgpln1');
file.general_optophysiology.set('imgpln2',...
types.core.ImagingPlane(...
'description', 'a different imaging plane',...
'device', oldImagingPlane.device,...
'optchan1', oldImagingPlane.opticalchannel.value,...
'excitation_lambda', 1,...
'imaging_rate', 2,...
'indicator', 'ASL',...
'location', 'somewhere else in the brain'));

hTwoPhotonSeries = file.acquisition.get('test_2ps');
hTwoPhotonSeries.imaging_plane.path = '/general/optophysiology/imgpln2';
hTwoPhotonSeries.data = hTwoPhotonSeries.data + rand();
end
end
end

Loading

0 comments on commit 3353784

Please sign in to comment.