diff --git a/+file/fillClass.m b/+file/fillClass.m index 41b55a4f..9ebf472b 100644 --- a/+file/fillClass.m +++ b/+file/fillClass.m @@ -93,7 +93,7 @@ superclassNames{end+1} = 'types.untyped.DatasetClass'; end - if isa(class, 'file.Group') && class.hasAnonGroups + if isa(class, 'file.Group') && (class.hasAnonGroups || class.hasAnonData) superclassNames{end+1} = 'matnwb.mixin.HasUnnamedGroups'; end @@ -147,7 +147,7 @@ , propertyDefinitionBody ... }, newline); end - if isa(class, 'file.Group') && class.hasAnonGroups + if isa(class, 'file.Group') && (class.hasAnonGroups || class.hasAnonData) mixinPropertyBlock = createPropertyBlockForHasUnnamedGroupMixin(class); fullPropertyDefinition = strjoin(... @@ -182,6 +182,7 @@ fullMethodBody = strjoin({'methods' ... file.addSpaces(methodBody, 4) 'end'}, newline); + template = strjoin({classDefinitionHeader fullPropertyDefinition fullMethodBody 'end'}, ... [newline newline]); end @@ -201,10 +202,15 @@ function propertyBlockStr = createPropertyBlockForHasUnnamedGroupMixin(classInfo) isAnonGroup = arrayfun(@(x) isempty(x.name), classInfo.subgroups, 'uni', true); - anonNames = arrayfun(@(x) lower(x.type), classInfo.subgroups(isAnonGroup), 'uni', false); - + isAnonDataset = arrayfun(@(x) isempty(x.name), classInfo.datasets, 'uni', true); + + anonNames = [... + arrayfun(@(x) lower(x.type), classInfo.subgroups(isAnonGroup), 'uni', false), ... + arrayfun(@(x) lower(x.type), classInfo.datasets(isAnonDataset), 'uni', false), ... + ]; + propertyBlockStr = strjoin({... - 'properties (Access = protected)', ... + 'properties (Constant, Access = private)', ... sprintf(' GroupPropertyNames = {%s}', strjoin(strcat('''', anonNames, ''''), ', ') ), ... 'end'}, newline); end diff --git a/+file/fillConstructor.m b/+file/fillConstructor.m index 020ec2ca..eaebabd4 100644 --- a/+file/fillConstructor.m +++ b/+file/fillConstructor.m @@ -169,7 +169,7 @@ fullBody = strjoin(fullBody, newline); bodystr(end+1:end+length(fullBody)+1) = [newline fullBody]; - if isa(class, 'file.Group') && class.hasAnonGroups + if isa(class, 'file.Group') && (class.hasAnonGroups || class.hasAnonData) % Include the setup function for the HasUnnamedGroups mixin bodystr = [bodystr, newline, 'obj.setupHasUnnamedGroupsMixin()', newline]; end diff --git a/+matnwb/+mixin/HasUnnamedGroups.m b/+matnwb/+mixin/HasUnnamedGroups.m index d0bccefa..bd8acf60 100644 --- a/+matnwb/+mixin/HasUnnamedGroups.m +++ b/+matnwb/+mixin/HasUnnamedGroups.m @@ -46,9 +46,14 @@ % are no schemas in NWB where Anon sets are used, and this class therefore % does not support contained Anon sets. - properties (Abstract, Access = protected, Transient) + properties (Access = private, Dependent, Transient) % GroupPropertyNames - String array of property names that contain Sets GroupPropertyNames (1,:) string + end + + properties (Access = private) + % GroupPropertyNames_ - Cached value backing the dependent property GroupPropertyNames + GroupPropertyNames_ (1,:) string end properties (Access = private, Transient) @@ -153,6 +158,17 @@ function setupHasUnnamedGroupsMixin(obj) obj.assignContainedSetCallbackFunctions() end end + + methods + function value = get.GroupPropertyNames(obj) + if isempty(obj.GroupPropertyNames_) + className = class(obj); + obj.GroupPropertyNames_ = obj.getGroupPropertyNamesAcrossTypeHierarchy(className); + end + value = obj.GroupPropertyNames_; + end + end + methods (Access = private) function initializeDynamicProperties(obj) % initializeDynamicProperties - Init dynamic properties from set entries @@ -515,6 +531,59 @@ function displayAliasWarning(obj) end end end + + methods (Static, Access = protected) + function groupPropertyNames = getGroupPropertyNamesAcrossTypeHierarchy(nwbTypeName) + % getGroupPropertyNamesAcrossTypeHierarchy - Retrieve property names of unnamed groups for a specific NWB type + % + % Syntax: + % groupTypes = getGroupPropertyNamesAcrossTypeHierarchy(nwbTypeName) + % This function retrieves property names of unnamed groups associated with + % the specified NWB type name, traversing the class hierarchy to also include + % property names of unnamed groups for parent types. + % + % Input Arguments: + % nwbTypeName (1,1) string - The name of the NWB type for which property + % names of unnamed groups are to be retrieved. + % + % Output Arguments: + % groupPropertyNames - An array of property names of unnamed groups + % associated with the specified NWB type. + + arguments + nwbTypeName (1,1) string + end + + groupPropertyNames = string.empty; % Initialize an empty cell array + currentType = nwbTypeName; % Start with the specific type + + % Iterate over class and superclasses to detect property names for + % unnamed groups across the type hierarchy. + while ~strcmp(currentType, 'types.untyped.MetaClass') + + % Use MetaClass information to get class information + metaClass = meta.class.fromName(currentType); + + % Get value of GroupPropertyNames if this class is a subclass of + % the HasUnnamedGroups subclass. + if any(strcmp({metaClass.SuperclassList.Name}, 'matnwb.mixin.HasUnnamedGroups')) + isProp = strcmp({metaClass.PropertyList.Name}, 'GroupPropertyNames'); + if any(isProp) + groupPropertyNames = [groupPropertyNames, ... + string(metaClass.PropertyList(isProp).DefaultValue)]; %#ok + end + end + + if isempty(metaClass.SuperclassList) + break; % Reached the base type + end + + % Get superclass for next iteration. NWB parent type should + % always be the first superclass in the list + currentType = metaClass.SuperclassList(1).Name; + end + end + end end function ME = getNameExistsException(name, typeName) diff --git a/+types/+core/BehavioralEpochs.m b/+types/+core/BehavioralEpochs.m index 2d00c31d..2ecdb6d3 100644 --- a/+types/+core/BehavioralEpochs.m +++ b/+types/+core/BehavioralEpochs.m @@ -9,7 +9,7 @@ properties intervalseries; % REQUIRED (IntervalSeries) IntervalSeries object containing start and stop times of epochs. end -properties (Access = protected) +properties (Constant, Access = private) GroupPropertyNames = {'intervalseries'} end diff --git a/+types/+core/BehavioralEvents.m b/+types/+core/BehavioralEvents.m index 8ee46485..e617565c 100644 --- a/+types/+core/BehavioralEvents.m +++ b/+types/+core/BehavioralEvents.m @@ -9,7 +9,7 @@ properties timeseries; % REQUIRED (TimeSeries) TimeSeries object containing behavioral events. end -properties (Access = protected) +properties (Constant, Access = private) GroupPropertyNames = {'timeseries'} end diff --git a/+types/+core/BehavioralTimeSeries.m b/+types/+core/BehavioralTimeSeries.m index 46f63a44..a1107a84 100644 --- a/+types/+core/BehavioralTimeSeries.m +++ b/+types/+core/BehavioralTimeSeries.m @@ -9,7 +9,7 @@ properties timeseries; % REQUIRED (TimeSeries) TimeSeries object containing continuous behavioral data. end -properties (Access = protected) +properties (Constant, Access = private) GroupPropertyNames = {'timeseries'} end diff --git a/+types/+core/CompassDirection.m b/+types/+core/CompassDirection.m index ab2bf141..13407a86 100644 --- a/+types/+core/CompassDirection.m +++ b/+types/+core/CompassDirection.m @@ -9,7 +9,7 @@ properties spatialseries; % REQUIRED (SpatialSeries) SpatialSeries object containing direction of gaze travel. end -properties (Access = protected) +properties (Constant, Access = private) GroupPropertyNames = {'spatialseries'} end diff --git a/+types/+core/DfOverF.m b/+types/+core/DfOverF.m index 2e9aad41..935c36bd 100644 --- a/+types/+core/DfOverF.m +++ b/+types/+core/DfOverF.m @@ -9,7 +9,7 @@ properties roiresponseseries; % REQUIRED (RoiResponseSeries) RoiResponseSeries object(s) containing dF/F for a ROI. end -properties (Access = protected) +properties (Constant, Access = private) GroupPropertyNames = {'roiresponseseries'} end diff --git a/+types/+core/EventWaveform.m b/+types/+core/EventWaveform.m index a2385e8c..f8601046 100644 --- a/+types/+core/EventWaveform.m +++ b/+types/+core/EventWaveform.m @@ -9,7 +9,7 @@ properties spikeeventseries; % REQUIRED (SpikeEventSeries) SpikeEventSeries object(s) containing detected spike event waveforms. end -properties (Access = protected) +properties (Constant, Access = private) GroupPropertyNames = {'spikeeventseries'} end diff --git a/+types/+core/EyeTracking.m b/+types/+core/EyeTracking.m index a91e9eb1..ec7256ca 100644 --- a/+types/+core/EyeTracking.m +++ b/+types/+core/EyeTracking.m @@ -9,7 +9,7 @@ properties spatialseries; % REQUIRED (SpatialSeries) SpatialSeries object containing data measuring direction of gaze. end -properties (Access = protected) +properties (Constant, Access = private) GroupPropertyNames = {'spatialseries'} end diff --git a/+types/+core/FilteredEphys.m b/+types/+core/FilteredEphys.m index c5038249..e622bb38 100644 --- a/+types/+core/FilteredEphys.m +++ b/+types/+core/FilteredEphys.m @@ -9,7 +9,7 @@ properties electricalseries; % REQUIRED (ElectricalSeries) ElectricalSeries object(s) containing filtered electrophysiology data. end -properties (Access = protected) +properties (Constant, Access = private) GroupPropertyNames = {'electricalseries'} end diff --git a/+types/+core/Fluorescence.m b/+types/+core/Fluorescence.m index ac09297b..1a695225 100644 --- a/+types/+core/Fluorescence.m +++ b/+types/+core/Fluorescence.m @@ -9,7 +9,7 @@ properties roiresponseseries; % REQUIRED (RoiResponseSeries) RoiResponseSeries object(s) containing fluorescence data for a ROI. end -properties (Access = protected) +properties (Constant, Access = private) GroupPropertyNames = {'roiresponseseries'} end diff --git a/+types/+core/ImageSegmentation.m b/+types/+core/ImageSegmentation.m index 11801451..0202c1f3 100644 --- a/+types/+core/ImageSegmentation.m +++ b/+types/+core/ImageSegmentation.m @@ -9,7 +9,7 @@ properties planesegmentation; % REQUIRED (PlaneSegmentation) Results from image segmentation of a specific imaging plane. end -properties (Access = protected) +properties (Constant, Access = private) GroupPropertyNames = {'planesegmentation'} end diff --git a/+types/+core/Images.m b/+types/+core/Images.m index 458245ae..87be552c 100644 --- a/+types/+core/Images.m +++ b/+types/+core/Images.m @@ -1,4 +1,4 @@ -classdef Images < types.core.NWBDataInterface & types.untyped.GroupClass +classdef Images < types.core.NWBDataInterface & types.untyped.GroupClass & matnwb.mixin.HasUnnamedGroups % IMAGES - A collection of images with an optional way to specify the order of the images using the "order_of_images" dataset. An order must be specified if the images are referenced by index, e.g., from an IndexSeries. % % Required Properties: @@ -14,6 +14,9 @@ properties order_of_images; % (ImageReferences) Ordered dataset of references to BaseImage objects stored in the parent group. Each object in the Images group should be stored once and only once, so the dataset should have the same length as the number of images. end +properties (Constant, Access = private) + GroupPropertyNames = {'baseimage'} +end methods function obj = Images(varargin) @@ -38,6 +41,8 @@ [obj.baseimage, ivarargin] = types.util.parseConstrained(obj,'baseimage', 'types.core.BaseImage', varargin{:}); varargin(ivarargin) = []; + obj.setupHasUnnamedGroupsMixin() + p = inputParser; p.KeepUnmatched = true; p.PartialMatching = false; diff --git a/+types/+core/ImagingPlane.m b/+types/+core/ImagingPlane.m index 426fd3b9..42f15ed4 100644 --- a/+types/+core/ImagingPlane.m +++ b/+types/+core/ImagingPlane.m @@ -26,7 +26,7 @@ origin_coords_unit = "meters"; % (char) Measurement units for origin_coords. The default value is 'meters'. reference_frame; % (char) Describes reference frame of origin_coords and grid_spacing. For example, this can be a text description of the anatomical location and orientation of the grid defined by origin_coords and grid_spacing or the vectors needed to transform or rotate the grid to a common anatomical axis (e.g., AP/DV/ML). This field is necessary to interpret origin_coords and grid_spacing. If origin_coords and grid_spacing are not present, then this field is not required. For example, if the microscope takes 10 x 10 x 2 images, where the first value of the data matrix (index (0, 0, 0)) corresponds to (-1.2, -0.6, -2) mm relative to bregma, the spacing between pixels is 0.2 mm in x, 0.2 mm in y and 0.5 mm in z, and larger numbers in x means more anterior, larger numbers in y means more rightward, and larger numbers in z means more ventral, then enter the following -- origin_coords = (-1.2, -0.6, -2) grid_spacing = (0.2, 0.2, 0.5) reference_frame = "Origin coordinates are relative to bregma. First dimension corresponds to anterior-posterior axis (larger index = more anterior). Second dimension corresponds to medial-lateral axis (larger index = more rightward). Third dimension corresponds to dorsal-ventral axis (larger index = more ventral)." end -properties (Access = protected) +properties (Constant, Access = private) GroupPropertyNames = {'opticalchannel'} end diff --git a/+types/+core/LFP.m b/+types/+core/LFP.m index 921dedc4..09e4823f 100644 --- a/+types/+core/LFP.m +++ b/+types/+core/LFP.m @@ -9,7 +9,7 @@ properties electricalseries; % REQUIRED (ElectricalSeries) ElectricalSeries object(s) containing LFP data for one or more channels. end -properties (Access = protected) +properties (Constant, Access = private) GroupPropertyNames = {'electricalseries'} end diff --git a/+types/+core/MotionCorrection.m b/+types/+core/MotionCorrection.m index e3ed97e8..694caebb 100644 --- a/+types/+core/MotionCorrection.m +++ b/+types/+core/MotionCorrection.m @@ -9,7 +9,7 @@ properties correctedimagestack; % REQUIRED (CorrectedImageStack) Results from motion correction of an image stack. end -properties (Access = protected) +properties (Constant, Access = private) GroupPropertyNames = {'correctedimagestack'} end diff --git a/+types/+core/Position.m b/+types/+core/Position.m index fe546460..92c3d03a 100644 --- a/+types/+core/Position.m +++ b/+types/+core/Position.m @@ -9,7 +9,7 @@ properties spatialseries; % REQUIRED (SpatialSeries) SpatialSeries object containing position data. end -properties (Access = protected) +properties (Constant, Access = private) GroupPropertyNames = {'spatialseries'} end diff --git a/+types/+core/ProcessingModule.m b/+types/+core/ProcessingModule.m index a87fd733..10b57807 100644 --- a/+types/+core/ProcessingModule.m +++ b/+types/+core/ProcessingModule.m @@ -14,7 +14,7 @@ dynamictable; % (DynamicTable) Tables stored in this collection. nwbdatainterface; % (NWBDataInterface) Data objects stored in this collection. end -properties (Access = protected) +properties (Constant, Access = private) GroupPropertyNames = {'nwbdatainterface', 'dynamictable'} end diff --git a/+types/+core/PupilTracking.m b/+types/+core/PupilTracking.m index 4e066bf5..791667cd 100644 --- a/+types/+core/PupilTracking.m +++ b/+types/+core/PupilTracking.m @@ -9,7 +9,7 @@ properties timeseries; % REQUIRED (TimeSeries) TimeSeries object containing time series data on pupil size. end -properties (Access = protected) +properties (Constant, Access = private) GroupPropertyNames = {'timeseries'} end diff --git a/+types/+hdmf_common/AlignedDynamicTable.m b/+types/+hdmf_common/AlignedDynamicTable.m index 892079b0..92b3b3b4 100644 --- a/+types/+hdmf_common/AlignedDynamicTable.m +++ b/+types/+hdmf_common/AlignedDynamicTable.m @@ -13,7 +13,7 @@ properties dynamictable; % (DynamicTable) A DynamicTable representing a particular category for columns in the AlignedDynamicTable parent container. The table MUST be aligned with (i.e., have the same number of rows) as all other DynamicTables stored in the AlignedDynamicTable parent container. The name of the category is given by the name of the DynamicTable and its description by the description attribute of the DynamicTable. end -properties (Access = protected) +properties (Constant, Access = private) GroupPropertyNames = {'dynamictable'} end diff --git a/+types/+hdmf_common/DynamicTable.m b/+types/+hdmf_common/DynamicTable.m index 1126c495..215bf809 100644 --- a/+types/+hdmf_common/DynamicTable.m +++ b/+types/+hdmf_common/DynamicTable.m @@ -1,4 +1,4 @@ -classdef DynamicTable < types.hdmf_common.Container & types.untyped.GroupClass +classdef DynamicTable < types.hdmf_common.Container & types.untyped.GroupClass & matnwb.mixin.HasUnnamedGroups % DYNAMICTABLE - A group containing multiple datasets that are aligned on the first dimension (Currently, this requirement if left up to APIs to check and enforce). These datasets represent different columns in the table. Apart from a column that contains unique identifiers for each row, there are no other required datasets. Users are free to add any number of custom VectorData objects (columns) here. DynamicTable also supports ragged array columns, where each element can be of a different size. To add a ragged array column, use a VectorIndex type to index the corresponding VectorData type. See documentation for VectorData and VectorIndex for more details. Unlike a compound data type, which is analogous to storing an array-of-structs, a DynamicTable can be thought of as a struct-of-arrays. This provides an alternative structure to choose from when optimizing storage for anticipated access patterns. Additionally, this type provides a way of creating a table without having to define a compound type up front. Although this convenience may be attractive, users should think carefully about how data will be accessed. DynamicTable is more appropriate for column-centric access, whereas a dataset with a compound type would be more appropriate for row-centric access. Finally, data size should also be taken into account. For small tables, performance loss may be an acceptable trade-off for the flexibility of a DynamicTable. % % Required Properties: @@ -15,6 +15,9 @@ properties vectordata; % (VectorData) Vector columns, including index columns, of this dynamic table. end +properties (Constant, Access = private) + GroupPropertyNames = {'vectordata'} +end methods function obj = DynamicTable(varargin) @@ -41,6 +44,8 @@ [obj.vectordata, ivarargin] = types.util.parseConstrained(obj,'vectordata', 'types.hdmf_common.VectorData', varargin{:}); varargin(ivarargin) = []; + obj.setupHasUnnamedGroupsMixin() + p = inputParser; p.KeepUnmatched = true; p.PartialMatching = false; diff --git a/+types/+hdmf_common/SimpleMultiContainer.m b/+types/+hdmf_common/SimpleMultiContainer.m index baa722ef..6e9ea5bf 100644 --- a/+types/+hdmf_common/SimpleMultiContainer.m +++ b/+types/+hdmf_common/SimpleMultiContainer.m @@ -10,8 +10,8 @@ container; % (Container) Container objects held within this SimpleMultiContainer. data; % (Data) Data objects held within this SimpleMultiContainer. end -properties (Access = protected) - GroupPropertyNames = {'container'} +properties (Constant, Access = private) + GroupPropertyNames = {'container', 'data'} end methods diff --git a/+types/+untyped/Anon.m b/+types/+untyped/Anon.m index c2e21a18..bb38f009 100644 --- a/+types/+untyped/Anon.m +++ b/+types/+untyped/Anon.m @@ -38,4 +38,21 @@ tf = strcmp(obj.name, name); end end -end \ No newline at end of file + + % Methods mirroring Set methods. + methods + function name = getPropertyName(obj, name) + % getPropertyName - Get property name given the actual name of an entry + assert(strcmp(obj.name, name), ... + 'NWB:Anon:InvalidName', ... + 'name `%s` is not part of Anon', name); + end + + function value = get(obj, name) + assert(strcmp(obj.name, name), ... + 'NWB:Anon:InvalidName', ... + 'name `%s` is not part of Anon', name); + value = obj.value; + end + end +end