Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a033816
Update fillClass.m
ehennestad Jan 24, 2026
8b6f76e
Refactor HasUnnamedGroups property handling and add hierarchy support
ehennestad Jan 24, 2026
441f9f6
Simplify - no need for method injection in subclasses of mixin
ehennestad Jan 24, 2026
b59661d
Regenerate types
ehennestad Jan 24, 2026
5e746fd
Simplify condition for HasUnnamedGroups mixin
ehennestad Jan 24, 2026
b2dda0d
Refine recursion logic in searchProperties function
ehennestad Jan 26, 2026
8138bf7
Merge branch 'fix-nwbfile-searchfor-bug' into unnamed-group-mixin-imp…
ehennestad Jan 26, 2026
f62aae6
Update fillConstructor.m
ehennestad Jan 27, 2026
428d8e6
Update Anon.m
ehennestad Jan 27, 2026
3ca7e02
Regenerate core
ehennestad Jan 27, 2026
575d5dd
Merge branch 'main' into unnamed-group-mixin-improvements
ehennestad Jan 27, 2026
7db3bc9
Refactor constructor validation and mixin setup logic
ehennestad Jan 27, 2026
d546781
Update fillConstructor.m
ehennestad Jan 27, 2026
d68004b
regenerate types
ehennestad Jan 27, 2026
80cddd7
fix spelling
ehennestad Jan 27, 2026
679c438
Merge branch 'refactor-constructor-generation' into unnamed-group-mix…
ehennestad Jan 27, 2026
dd3b6d6
regenerate type classes
ehennestad Jan 27, 2026
c1814e7
Update TutorialTest.m
ehennestad Mar 10, 2026
97a9665
Merge branch 'ignore-nwbinspector-subject-checks-in-tutorialtests' in…
ehennestad Mar 10, 2026
4040b93
Merge branch 'main' into unnamed-group-mixin-improvements
ehennestad Apr 1, 2026
aaf2c9a
Merge branch 'unnamed-group-mixin-improvements' of https://github.com…
ehennestad Apr 1, 2026
570ef0a
Merge branch 'main' into unnamed-group-mixin-improvements
ehennestad Apr 13, 2026
68740a8
Fix merge error
ehennestad Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions +file/fillClass.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(...
Expand Down Expand Up @@ -183,6 +183,7 @@

fullMethodBody = strjoin({'methods' ...
file.addSpaces(methodBody, 4) 'end'}, newline);

template = strjoin({classDefinitionHeader fullPropertyDefinition fullMethodBody 'end'}, ...
[newline newline]);
end
Expand All @@ -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
2 changes: 1 addition & 1 deletion +file/fillConstructor.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
71 changes: 70 additions & 1 deletion +matnwb/+mixin/HasUnnamedGroups.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<AGROW>
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)
Expand Down
2 changes: 1 addition & 1 deletion +types/+core/BehavioralEpochs.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion +types/+core/BehavioralEvents.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
properties
timeseries; % REQUIRED (TimeSeries) TimeSeries object containing behavioral events.
end
properties (Access = protected)
properties (Constant, Access = private)
GroupPropertyNames = {'timeseries'}
end

Expand Down
2 changes: 1 addition & 1 deletion +types/+core/BehavioralTimeSeries.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion +types/+core/CompassDirection.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion +types/+core/DfOverF.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion +types/+core/EventWaveform.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion +types/+core/EyeTracking.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion +types/+core/FilteredEphys.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion +types/+core/Fluorescence.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion +types/+core/ImageSegmentation.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 5 additions & 1 deletion +types/+core/Images.m
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -53,6 +56,7 @@
if strcmp(class(obj), 'types.core.Images') %#ok<STISA>
cellStringArguments = convertContainedStringsToChars(varargin(1:2:end));
types.util.checkUnset(obj, unique(cellStringArguments));
obj.setupHasUnnamedGroupsMixin();
end
end
%% SETTERS
Expand Down
2 changes: 1 addition & 1 deletion +types/+core/ImagingPlane.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion +types/+core/LFP.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion +types/+core/MotionCorrection.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion +types/+core/Position.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
properties
spatialseries; % REQUIRED (SpatialSeries) SpatialSeries object containing position data.
end
properties (Access = protected)
properties (Constant, Access = private)
GroupPropertyNames = {'spatialseries'}
end

Expand Down
2 changes: 1 addition & 1 deletion +types/+core/ProcessingModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion +types/+core/PupilTracking.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion +types/+core/Units.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<STISA>
Expand Down
2 changes: 1 addition & 1 deletion +types/+hdmf_common/AlignedDynamicTable.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 5 additions & 1 deletion +types/+hdmf_common/DynamicTable.m
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -58,6 +61,7 @@
if strcmp(class(obj), 'types.hdmf_common.DynamicTable') %#ok<STISA>
cellStringArguments = convertContainedStringsToChars(varargin(1:2:end));
types.util.checkUnset(obj, unique(cellStringArguments));
obj.setupHasUnnamedGroupsMixin();
types.util.dynamictable.checkConfig(obj);
end
end
Expand Down
4 changes: 2 additions & 2 deletions +types/+hdmf_common/SimpleMultiContainer.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion +types/+untyped/Anon.m
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,21 @@
tf = strcmp(obj.name, name);
end
end
end

% 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
Loading