Skip to content

Commit d47ff40

Browse files
authored
Fill in "general_was_generated_by" when exporting nwb file (#700)
* Create Contents.m * Add workflow file for preparing new release * Update worflow to run on a matrix of platforms * Update CloneNwbTest.m Add ad hoc pc compatibility fix * Update requirements.txt Some requirements are not present by default on windows runners * Update prepare_release.yml Fix problems running tests on mac and windows runners * Update prepare_release.yml Update job name * Update prepare_release.yml Use deploy key as this will job will push back to protected branch * Add page in docs detailing how to create a release * Update releases.rst Fix rst formatting * Update prepare_release.yml * Update NwbFile.m * Add unit test for read/export of was_generated_by property * Fix test * Try: Possibly a bug * Revert "Try: Possibly a bug" This reverts commit 3c78c19. * Ignore field "was_generated_by" in tests comparing MatNWB and PyNWB generated file * Fix bug where 2D datasets might get treated as vectors if their length is 1
1 parent 5f1b8cf commit d47ff40

File tree

9 files changed

+65
-15
lines changed

9 files changed

+65
-15
lines changed

+file/fillExport.m

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@
186186
options = [options {'''forceChunking''', '''forceArray'''}];
187187
elseif ~prop.scalar
188188
options = [options {'''forceArray'''}];
189+
% If dataset is 2D, need to force matrix-like
190+
if numel(prop.shape) == 2 && ~iscell(prop.shape{1})
191+
options = [options {'''forceMatrix'''}];
192+
end
189193
end
190194

191195
% untyped compound

+io/mapData2H5.m

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
% and properly converted data
55

66
forceArray = any(strcmp('forceArray', varargin));
7+
forceMatrix = any(strcmp('forceMatrix', varargin));
78
forceChunked = any(strcmp('forceChunking', varargin));
89

910
if iscell(data)
@@ -43,7 +44,7 @@
4344
elseif ~forceChunked && isempty(data)
4445
sid = H5S.create_simple(1, 0, 0);
4546
else
46-
if isvector(data) || isempty(data)
47+
if ~forceMatrix && (isvector(data) || isempty(data))
4748
num_dims = 1;
4849
dims = length(data);
4950
else

+tests/+system/PyNWBIOTest.m

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ function testInFromPyNWB(testCase)
2929
nwbExport(testCase.file, 'temp.nwb'); % hack to fill out ObjectView container paths.
3030
% ignore file_create_date because nwbExport will actually
3131
% mutate the property every export.
32-
tests.util.verifyContainerEqual(testCase, pycontainer, matcontainer, {'file_create_date'});
32+
% ignore general/was_generated_by because the value will be
33+
% specific to matnwb generated file.
34+
ignoreFields = {'file_create_date', 'general_was_generated_by'};
35+
tests.util.verifyContainerEqual(testCase, pycontainer, matcontainer, ignoreFields);
3336
end
3437
end
3538

+tests/+system/PyNWBIOTest.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ def container(self):
3333

3434
def testInFromMatNWB(self):
3535
filename = 'MatNWB.' + self.__class__.__name__ + '.testOutToPyNWB.nwb'
36-
with HDF5IO(filename, manager=get_manager(), mode='r') as io:
36+
with HDF5IO(filename, manager=get_manager(), mode='r+') as io:
3737
matfile = io.read()
3838
matcontainer = self.getContainer(matfile)
3939
pycontainer = self.getContainer(self.file)
40-
self.assertContainerEqual(matcontainer, pycontainer)
40+
# ignore was_generated_by because it will be specific to matnwb generated file
41+
self.assertContainerEqual(matcontainer, pycontainer, ignoreFields=["was_generated_by"])
4142

4243
def testOutToMatNWB(self):
4344
filename = 'PyNWB.' + self.__class__.__name__ + '.testOutToMatNWB.nwb'
@@ -51,7 +52,7 @@ def addContainer(self, file):
5152
def getContainer(self, file):
5253
raise unittest.SkipTest('Cannot run test unless getContainer is implemented')
5354

54-
def assertContainerEqual(self, container1, container2): # noqa: C901
55+
def assertContainerEqual(self, container1, container2, ignoreFields=[]): # noqa: C901
5556
'''
5657
container1 is what was read or generated
5758
container2 is what is hardcoded in the TestCase
@@ -64,6 +65,8 @@ def assertContainerEqual(self, container1, container2): # noqa: C901
6465
except AttributeError:
6566
container_fields = container1.__fields__
6667
for nwbfield in container_fields:
68+
if nwbfield in ignoreFields:
69+
continue
6770
with self.subTest(nwbfield=nwbfield, container_type=type1.__name__):
6871
field1 = getattr(container1, nwbfield)
6972
field2 = getattr(container2, nwbfield)

+tests/+unit/nwbExportTest.m

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,5 +286,23 @@ function testExportFileWithStringDataType(testCase)
286286
string(tsIn.data_unit), ...
287287
ts.data_unit)
288288
end
289+
290+
function testWasGeneratedByProperty(testCase)
291+
nwb = tests.factory.NWBFile();
292+
nwbFilename = testCase.getRandomFilename();
293+
nwbExport(nwb, nwbFilename);
294+
295+
nwbIn = nwbRead(nwbFilename, 'ignorecache');
296+
testCase.verifyTrue(any(contains(nwbIn.general_was_generated_by.load(), 'matnwb')))
297+
298+
% Export again
299+
nwbFilename2 = testCase.getRandomFilename();
300+
nwbExport(nwbIn, nwbFilename2);
301+
302+
nwbIn2 = nwbRead(nwbFilename2, 'ignorecache');
303+
304+
% Verify that was_generated_by still has one entry (i.e not getting duplicate entries)
305+
testCase.verifyEqual(size(nwbIn2.general_was_generated_by.load()), [2,1])
306+
end
289307
end
290308
end

+types/+core/ClusterWaveforms.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,12 @@
151151
if startsWith(class(obj.waveform_mean), 'types.untyped.')
152152
refs = obj.waveform_mean.export(fid, [fullpath '/waveform_mean'], refs);
153153
elseif ~isempty(obj.waveform_mean)
154-
io.writeDataset(fid, [fullpath '/waveform_mean'], obj.waveform_mean, 'forceArray');
154+
io.writeDataset(fid, [fullpath '/waveform_mean'], obj.waveform_mean, 'forceArray', 'forceMatrix');
155155
end
156156
if startsWith(class(obj.waveform_sd), 'types.untyped.')
157157
refs = obj.waveform_sd.export(fid, [fullpath '/waveform_sd'], refs);
158158
elseif ~isempty(obj.waveform_sd)
159-
io.writeDataset(fid, [fullpath '/waveform_sd'], obj.waveform_sd, 'forceArray');
159+
io.writeDataset(fid, [fullpath '/waveform_sd'], obj.waveform_sd, 'forceArray', 'forceMatrix');
160160
end
161161
end
162162
end

+types/+core/ImagingRetinotopy.m

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -994,7 +994,7 @@ function postset_vasculature_image_format(obj)
994994
if startsWith(class(obj.axis_1_phase_map), 'types.untyped.')
995995
refs = obj.axis_1_phase_map.export(fid, [fullpath '/axis_1_phase_map'], refs);
996996
elseif ~isempty(obj.axis_1_phase_map)
997-
io.writeDataset(fid, [fullpath '/axis_1_phase_map'], obj.axis_1_phase_map, 'forceArray');
997+
io.writeDataset(fid, [fullpath '/axis_1_phase_map'], obj.axis_1_phase_map, 'forceArray', 'forceMatrix');
998998
end
999999
if ~isempty(obj.axis_1_phase_map) && ~isa(obj.axis_1_phase_map, 'types.untyped.SoftLink') && ~isa(obj.axis_1_phase_map, 'types.untyped.ExternalLink')
10001000
io.writeAttribute(fid, [fullpath '/axis_1_phase_map/dimension'], obj.axis_1_phase_map_dimension, 'forceArray');
@@ -1015,7 +1015,7 @@ function postset_vasculature_image_format(obj)
10151015
if startsWith(class(obj.axis_1_power_map), 'types.untyped.')
10161016
refs = obj.axis_1_power_map.export(fid, [fullpath '/axis_1_power_map'], refs);
10171017
elseif ~isempty(obj.axis_1_power_map)
1018-
io.writeDataset(fid, [fullpath '/axis_1_power_map'], obj.axis_1_power_map, 'forceArray');
1018+
io.writeDataset(fid, [fullpath '/axis_1_power_map'], obj.axis_1_power_map, 'forceArray', 'forceMatrix');
10191019
end
10201020
end
10211021
if ~isempty(obj.axis_1_power_map) && ~isa(obj.axis_1_power_map, 'types.untyped.SoftLink') && ~isa(obj.axis_1_power_map, 'types.untyped.ExternalLink')
@@ -1045,7 +1045,7 @@ function postset_vasculature_image_format(obj)
10451045
if startsWith(class(obj.axis_2_phase_map), 'types.untyped.')
10461046
refs = obj.axis_2_phase_map.export(fid, [fullpath '/axis_2_phase_map'], refs);
10471047
elseif ~isempty(obj.axis_2_phase_map)
1048-
io.writeDataset(fid, [fullpath '/axis_2_phase_map'], obj.axis_2_phase_map, 'forceArray');
1048+
io.writeDataset(fid, [fullpath '/axis_2_phase_map'], obj.axis_2_phase_map, 'forceArray', 'forceMatrix');
10491049
end
10501050
if ~isempty(obj.axis_2_phase_map) && ~isa(obj.axis_2_phase_map, 'types.untyped.SoftLink') && ~isa(obj.axis_2_phase_map, 'types.untyped.ExternalLink')
10511051
io.writeAttribute(fid, [fullpath '/axis_2_phase_map/dimension'], obj.axis_2_phase_map_dimension, 'forceArray');
@@ -1066,7 +1066,7 @@ function postset_vasculature_image_format(obj)
10661066
if startsWith(class(obj.axis_2_power_map), 'types.untyped.')
10671067
refs = obj.axis_2_power_map.export(fid, [fullpath '/axis_2_power_map'], refs);
10681068
elseif ~isempty(obj.axis_2_power_map)
1069-
io.writeDataset(fid, [fullpath '/axis_2_power_map'], obj.axis_2_power_map, 'forceArray');
1069+
io.writeDataset(fid, [fullpath '/axis_2_power_map'], obj.axis_2_power_map, 'forceArray', 'forceMatrix');
10701070
end
10711071
end
10721072
if ~isempty(obj.axis_2_power_map) && ~isa(obj.axis_2_power_map, 'types.untyped.SoftLink') && ~isa(obj.axis_2_power_map, 'types.untyped.ExternalLink')
@@ -1102,7 +1102,7 @@ function postset_vasculature_image_format(obj)
11021102
if startsWith(class(obj.focal_depth_image), 'types.untyped.')
11031103
refs = obj.focal_depth_image.export(fid, [fullpath '/focal_depth_image'], refs);
11041104
elseif ~isempty(obj.focal_depth_image)
1105-
io.writeDataset(fid, [fullpath '/focal_depth_image'], obj.focal_depth_image, 'forceArray');
1105+
io.writeDataset(fid, [fullpath '/focal_depth_image'], obj.focal_depth_image, 'forceArray', 'forceMatrix');
11061106
end
11071107
end
11081108
if ~isempty(obj.focal_depth_image) && ~isa(obj.focal_depth_image, 'types.untyped.SoftLink') && ~isa(obj.focal_depth_image, 'types.untyped.ExternalLink')
@@ -1149,7 +1149,7 @@ function postset_vasculature_image_format(obj)
11491149
if startsWith(class(obj.sign_map), 'types.untyped.')
11501150
refs = obj.sign_map.export(fid, [fullpath '/sign_map'], refs);
11511151
elseif ~isempty(obj.sign_map)
1152-
io.writeDataset(fid, [fullpath '/sign_map'], obj.sign_map, 'forceArray');
1152+
io.writeDataset(fid, [fullpath '/sign_map'], obj.sign_map, 'forceArray', 'forceMatrix');
11531153
end
11541154
end
11551155
if ~isempty(obj.sign_map) && ~isa(obj.sign_map, 'types.untyped.SoftLink') && ~isa(obj.sign_map, 'types.untyped.ExternalLink')
@@ -1171,7 +1171,7 @@ function postset_vasculature_image_format(obj)
11711171
if startsWith(class(obj.vasculature_image), 'types.untyped.')
11721172
refs = obj.vasculature_image.export(fid, [fullpath '/vasculature_image'], refs);
11731173
elseif ~isempty(obj.vasculature_image)
1174-
io.writeDataset(fid, [fullpath '/vasculature_image'], obj.vasculature_image, 'forceArray');
1174+
io.writeDataset(fid, [fullpath '/vasculature_image'], obj.vasculature_image, 'forceArray', 'forceMatrix');
11751175
end
11761176
if ~isempty(obj.vasculature_image) && ~isa(obj.vasculature_image, 'types.untyped.SoftLink') && ~isa(obj.vasculature_image, 'types.untyped.ExternalLink')
11771177
io.writeAttribute(fid, [fullpath '/vasculature_image/bits_per_pixel'], obj.vasculature_image_bits_per_pixel);

+types/+core/NWBFile.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1190,7 +1190,7 @@ function postset_general_source_script_file_name(obj)
11901190
if startsWith(class(obj.general_was_generated_by), 'types.untyped.')
11911191
refs = obj.general_was_generated_by.export(fid, [fullpath '/general/was_generated_by'], refs);
11921192
elseif ~isempty(obj.general_was_generated_by)
1193-
io.writeDataset(fid, [fullpath '/general/was_generated_by'], obj.general_was_generated_by, 'forceArray');
1193+
io.writeDataset(fid, [fullpath '/general/was_generated_by'], obj.general_was_generated_by, 'forceArray', 'forceMatrix');
11941194
end
11951195
end
11961196
if startsWith(class(obj.identifier), 'types.untyped.')

NwbFile.m

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ function export(obj, filename, mode)
5555
obj.file_create_date(end+1) = current_time;
5656
end
5757

58+
obj.addWasGeneratedBy()
59+
5860
%equate reference time to session_start_time if empty
5961
if isempty(obj.timestamps_reference_time)
6062
obj.timestamps_reference_time = obj.session_start_time;
@@ -158,6 +160,25 @@ function export(obj, filename, mode)
158160

159161
%% PRIVATE
160162
methods(Access=private)
163+
function addWasGeneratedBy(obj)
164+
if isprop(obj, 'general_was_generated_by')
165+
if isa(obj.general_was_generated_by, 'types.untyped.DataStub')
166+
obj.general_was_generated_by = obj.general_was_generated_by.load();
167+
end
168+
169+
matnwbInfo = ver('matnwb');
170+
wasGeneratedBy = {'matnwb'; matnwbInfo.Version};
171+
172+
if isempty(obj.general_was_generated_by)
173+
obj.general_was_generated_by = wasGeneratedBy;
174+
else
175+
if ~any(contains(obj.general_was_generated_by(:), 'matnwb'))
176+
obj.general_was_generated_by(:, end+1) = wasGeneratedBy;
177+
end
178+
end
179+
end
180+
end
181+
161182
function embedSpecifications(obj, output_file_id)
162183
jsonSpecs = schemes.exportJson();
163184

0 commit comments

Comments
 (0)