diff --git a/+file/fillClass.m b/+file/fillClass.m index 96ecfe16..ff2b4adc 100644 --- a/+file/fillClass.m +++ b/+file/fillClass.m @@ -94,7 +94,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 @@ -148,7 +148,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(... @@ -183,6 +183,7 @@ fullMethodBody = strjoin({'methods' ... file.addSpaces(methodBody, 4) 'end'}, newline); + template = strjoin({classDefinitionHeader fullPropertyDefinition fullMethodBody 'end'}, ... [newline newline]); end @@ -202,10 +203,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 42f67cbd..2a893d88 100644 --- a/+file/fillConstructor.m +++ b/+file/fillConstructor.m @@ -23,7 +23,7 @@ ' types.util.checkUnset(obj, unique(cellStringArguments));'}; % Include the setup function for the HasUnnamedGroups mixin if applicable - if isa(class, 'file.Group') && class.hasAnonGroups + if isa(class, 'file.Group') && (class.hasAnonGroups || class.hasAnonData) constructorElements{end+1} = ' obj.setupHasUnnamedGroupsMixin();'; 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 80d8287e..8e75a8d4 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 78cff2ee..49ec57a5 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 546fbec2..3b9d6d02 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 afe9acb9..7fb0eed7 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 a001b9ca..94e5e461 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 91902fdb..cc0648c6 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 57c645c5..778ab16c 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 94258869..d8ae9687 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 6e265ef5..f1919944 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 98db9490..35cd7a9d 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 3f75cb23..f60e065d 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) @@ -53,6 +56,7 @@ if strcmp(class(obj), 'types.core.Images') %#ok cellStringArguments = convertContainedStringsToChars(varargin(1:2:end)); types.util.checkUnset(obj, unique(cellStringArguments)); + obj.setupHasUnnamedGroupsMixin(); end end %% SETTERS diff --git a/+types/+core/ImagingPlane.m b/+types/+core/ImagingPlane.m index 415b84b3..dd29c5c1 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 001ab0c6..f993e733 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 124ffd92..1ce5b2a0 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 9ca0fb84..f3b173cd 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 13c18b01..e783a63e 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 c49f1223..7e67cb76 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/+core/Units.m b/+types/+core/Units.m index e6af0fb0..a09fa714 100644 --- a/+types/+core/Units.m +++ b/+types/+core/Units.m @@ -131,7 +131,7 @@ obj.waveforms_index_index = p.Results.waveforms_index_index; obj.waveforms_sampling_rate = p.Results.waveforms_sampling_rate; obj.waveforms_unit = p.Results.waveforms_unit; - + % Only execute validation/setup code when called directly in this class's % constructor, not when invoked through superclass constructor chain if strcmp(class(obj), 'types.core.Units') %#ok diff --git a/+types/+hdmf_common/AlignedDynamicTable.m b/+types/+hdmf_common/AlignedDynamicTable.m index 7907b4d9..c8a64c02 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 41bd4b43..9a9d5a09 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) @@ -58,6 +61,7 @@ if strcmp(class(obj), 'types.hdmf_common.DynamicTable') %#ok cellStringArguments = convertContainedStringsToChars(varargin(1:2:end)); types.util.checkUnset(obj, unique(cellStringArguments)); + obj.setupHasUnnamedGroupsMixin(); types.util.dynamictable.checkConfig(obj); end end diff --git a/+types/+hdmf_common/SimpleMultiContainer.m b/+types/+hdmf_common/SimpleMultiContainer.m index 947bd848..c1f45599 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