From 4fcf3d9f76b2d17cdacb160313ebd59fda410b3d Mon Sep 17 00:00:00 2001 From: bendichter Date: Mon, 14 Jan 2019 23:41:45 +0100 Subject: [PATCH 1/7] changes to schema --- schema/core/nwb.base.yaml | 24 +++++++-- schema/core/nwb.behavior.yaml | 10 ++-- schema/core/nwb.ecephys.yaml | 19 +++++++ schema/core/nwb.file.yaml | 8 +++ schema/core/nwb.misc.yaml | 98 +++++++++++++++++++++++++++++++++- schema/core/nwb.namespace.yaml | 2 +- schema/core/nwb.ogen.yaml | 13 ++--- schema/core/nwb.ophys.yaml | 16 +++--- 8 files changed, 169 insertions(+), 21 deletions(-) diff --git a/schema/core/nwb.base.yaml b/schema/core/nwb.base.yaml index 5769cda7..bd130b55 100644 --- a/schema/core/nwb.base.yaml +++ b/schema/core/nwb.base.yaml @@ -168,9 +168,27 @@ groups: such as Volts. If the actual data is stored in millivolts, the field ''conversion'' below describes how to convert the data to the specified SI unit.' dims: - - num_times + - - num_times + - - num_times + - num_DIM2 + - - num_times + - num_DIM2 + - num_DIM3 + - - num_times + - num_DIM2 + - num_DIM3 + - num_DIM4 shape: - - null + - - null + - - null + - null + - - null + - null + - null + - - null + - null + - null + - null - name: starting_time dtype: float64 doc: 'The timestamp of the first sample. COMMENT: When timestamps are uniformly @@ -274,7 +292,7 @@ groups: doc: Value is 'A column-centric table' value: A column-centric table - name: colnames - dtype: text + dtype: ascii doc: The names of the columns in this table. This should be used to specifying an order to the columns shape: diff --git a/schema/core/nwb.behavior.yaml b/schema/core/nwb.behavior.yaml index d0bc933d..6f8222d3 100644 --- a/schema/core/nwb.behavior.yaml +++ b/schema/core/nwb.behavior.yaml @@ -31,11 +31,13 @@ groups: default_value: meter required: false dims: - - num_times - - num_features + - - num_times + - - num_times + - num_features shape: - - null - - null + - - null + - - null + - null - name: reference_frame dtype: text doc: Description defining what exactly 'straight-ahead' means. diff --git a/schema/core/nwb.ecephys.yaml b/schema/core/nwb.ecephys.yaml index b7f7bcd0..82806f31 100644 --- a/schema/core/nwb.ecephys.yaml +++ b/schema/core/nwb.ecephys.yaml @@ -73,6 +73,25 @@ groups: - - null - null - null + - name: timestamps + dtype: float64 + doc: 'Timestamps for samples stored in data.COMMENT: Timestamps here have all + been corrected to the common experiment master-clock. Time is stored as seconds + and all timestamps are relative to experiment start time. This is added here so + that the timestamps is required for SpikeEventTimes.' + attributes: + - name: interval + dtype: int32 + doc: Value is '1' + value: 1 + - name: unit + dtype: text + doc: Value is 'Seconds' + value: Seconds + dims: + - num_times + shape: + - null - neurodata_type_def: ClusterWaveforms neurodata_type_inc: NWBDataInterface doc: 'The mean waveform shape, including standard deviation, of the different clusters. diff --git a/schema/core/nwb.file.yaml b/schema/core/nwb.file.yaml index 9b9e7936..ab509807 100644 --- a/schema/core/nwb.file.yaml +++ b/schema/core/nwb.file.yaml @@ -223,6 +223,10 @@ groups: date made, injection location, volume, etc. quantity: '?' groups: + - neurodata_type_def: LabMetaData + neurodata_type_inc: NWBContainer + doc: 'place-holder than can be extended so that lab-specific meta-data can be placed in /general' + quantity: '*' - name: devices doc: 'Description of hardware devices used during experiment. COMMENT: Eg, monitors, ADC boards, microscopes, etc' @@ -273,6 +277,10 @@ groups: dtype: text doc: Age of subject quantity: '?' + - name: date_of_birth + dtype: isodatetime + doc: can be supplied instead of age + quantity: '?' - name: description dtype: text doc: Description of subject and where subject came from (e.g., breeder, if diff --git a/schema/core/nwb.misc.yaml b/schema/core/nwb.misc.yaml index 6c909bc9..aa489dad 100644 --- a/schema/core/nwb.misc.yaml +++ b/schema/core/nwb.misc.yaml @@ -111,6 +111,86 @@ groups: - num_times shape: - null +- neurodata_type_def: DecompositionSeries + neurodata_type_inc: TimeSeries + doc: Holds spectral analysis of a timeseries. For instance of LFP or a speech signal + datasets: + - name: data + dtype: float + doc: The data goes here + shape: + - null + - null + - null + dims: + - num_times + - num_channels + - num_bands + - name: metric + dtype: text + doc: 'recommended: phase, amplitude, power' + links: + - name: source_timeseries + doc: HDF5 link to TimesSeries that this data was calculated from. Metadata + about electrodes and their position can be read from that ElectricalSeries so + it's not necessary to store that information here + target_type: TimeSeries + groups: + - neurodata_type_inc: DynamicTable + name: bands + doc: A table for describing the bands that this series was generated from. There + should be one row in this table for each band + datasets: + - neurodata_type_inc: VectorData + name: band_name + dtype: text + doc: the name of the band e.g. theta + attributes: + - name: description + dtype: text + doc: value is 'the name of the band e.g. theta' + value: the name of the band e.g. theta + - neurodata_type_inc: VectorData + name: band_limits + dtype: float + shape: + - null + - 2 + dims: + - num_bands + - low, high + doc: Low and high limit of each band in Hz. If it is a Gaussian filter, use 2 SD on either side of the center + attributes: + - name: description + dtype: text + doc: value is 'Low and high limit of each band in Hz. If it is a Gaussian filter, use 2 SD on either side of the center' + value: Low and high limit of each band in Hz. If it is a Gaussian filter, use 2 SD on either side of the center + - neurodata_type_inc: VectorData + name: band_mean + dtype: float + shape: + - null + dims: + - num_bands + doc: The mean Gaussian filters in Hz + attributes: + - name: description + dtype: text + doc: The mean Gaussian filters in Hz + value: The mean Gaussian filters in Hz + - neurodata_type_inc: VectorData + name: band_stdev + dtype: float + shape: + - null + dims: + - num_bands + doc: The standard devaition of Gaussian filters in Hz + attributes: + - name: description + dtype: text + doc: The standard devaition of Gaussian filters in Hz + value: The standard devaition of Gaussian filters in Hz - neurodata_type_def: Units neurodata_type_inc: DynamicTable doc: Data about spiking units. Event times of observed units (e.g. cell, synapse, etc.) @@ -127,9 +207,25 @@ groups: quantity: '?' - neurodata_type_inc: VectorData name: spike_times + dtype: double doc: the spike times for each unit quantity: '?' - - neurodata_type_inc: DynamicTableRegion + - neurodata_type_inc: VectorIndex + name: obs_intervals_index + doc: the index into the obs_intervals dataset + quantity: '?' + - neurodata_type_inc: VectorData + name: obs_intervals + doc: the observation intervals for each unit + quantity: '?' + dtype: double + dims: + - num_intervals + - start|end + shape: + - null + - 2 + - neurodata_type_inc: VectorIndex name: electrodes_index doc: the index into electrodes quantity: '?' diff --git a/schema/core/nwb.namespace.yaml b/schema/core/nwb.namespace.yaml index 9f604789..81af3069 100644 --- a/schema/core/nwb.namespace.yaml +++ b/schema/core/nwb.namespace.yaml @@ -24,4 +24,4 @@ namespaces: - source: nwb.ogen.yaml - source: nwb.ophys.yaml - source: nwb.retinotopy.yaml - version: 1.2.0 + version: 2.0.0b diff --git a/schema/core/nwb.ogen.yaml b/schema/core/nwb.ogen.yaml index 9c54c2f8..999059d2 100644 --- a/schema/core/nwb.ogen.yaml +++ b/schema/core/nwb.ogen.yaml @@ -31,7 +31,7 @@ groups: target_type: OptogeneticStimulusSite - neurodata_type_def: OptogeneticStimulusSite neurodata_type_inc: NWBContainer - doc: 'One of possibly many groups describing an optogenetic stimuluation site. COMMENT: + doc: 'One of possibly many groups describing an optogenetic stimulation site. COMMENT: Name is arbitrary but should be meaningful. Name is referenced by OptogeneticSeries' attributes: - name: help @@ -42,13 +42,14 @@ groups: - name: description dtype: text doc: Description of site - - name: device - dtype: text - doc: Name of device in /general/devices - name: excitation_lambda - dtype: text - doc: Excitation wavelength + dtype: float + doc: Excitation wavelength in nm - name: location dtype: text doc: Location of stimulation site quantity: '*' + links: + - name: device + doc: Device that generated the stimulus + target_type: Device diff --git a/schema/core/nwb.ophys.yaml b/schema/core/nwb.ophys.yaml index 505689f3..6e4c3d31 100644 --- a/schema/core/nwb.ophys.yaml +++ b/schema/core/nwb.ophys.yaml @@ -49,11 +49,13 @@ groups: dtype: float32 doc: Signals from ROIs dims: - - num_times - - num_ROIs + - - num_times + - - num_times + - num_ROIs shape: - - null - - null + - - null + - - null + - null - neurodata_type_inc: DynamicTableRegion name: rois doc: a dataset referencing into an ROITable containing information on the ROIs @@ -73,7 +75,7 @@ groups: groups: - neurodata_type_inc: RoiResponseSeries doc: RoiResponseSeries object containing dF/F for a ROI - quantity: '*' + quantity: '+' default_name: DfOverF - neurodata_type_def: Fluorescence neurodata_type_inc: NWBDataInterface @@ -189,11 +191,12 @@ groups: - neurodata_type_inc: ImageSeries doc: One or more image stacks that the masks apply to (can be one-element stack) + quantity: '*' links: - name: imaging_plane doc: link to ImagingPlane group from which this TimeSeries data was generated target_type: ImagingPlane - quantity: '*' + quantity: '+' default_name: ImageSegmentation - neurodata_type_def: ImagingPlane neurodata_type_inc: NWBContainer @@ -254,6 +257,7 @@ groups: - null - null - 3 + quantity: '?' - name: reference_frame dtype: text doc: 'Describes position and reference frame of manifold based on position of From 6b68fd02705f61616cc97f17cb6532d13919677f Mon Sep 17 00:00:00 2001 From: bendichter Date: Tue, 15 Jan 2019 16:44:38 +0100 Subject: [PATCH 2/7] update convertTrials * change OgenStimSite.excitation_lambda str to float * change OgenStimSite.device from str to link --- tutorials/convertTrials.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tutorials/convertTrials.m b/tutorials/convertTrials.m index a4b85d34..a4cdc8e2 100644 --- a/tutorials/convertTrials.m +++ b/tutorials/convertTrials.m @@ -242,9 +242,9 @@ % general/optogenetics/photostim nwb.general_optogenetics.set('photostim', ... types.core.OptogeneticStimulusSite(... - 'excitation_lambda', num2str(meta.photostim.photostimWavelength{1}), ... + 'excitation_lambda', meta.photostim.photostimWavelength{1}, ... 'location', meta.photostim.photostimLocation{1}, ... - 'device', laserName, ... + 'device', types.untyped.SoftLink(['/general/devices/' laserName]), ... 'description', formatStruct(meta.photostim, {... 'stimulationMethod';'photostimCoordinates';'identificationMethod'}))); %% Analysis Data Structure From e26e1965e73bae466920cfdae640740dfa690810 Mon Sep 17 00:00:00 2001 From: bendichter Date: Tue, 15 Jan 2019 16:50:06 +0100 Subject: [PATCH 3/7] update schema --- schema/core/nwb.ecephys.yaml | 4 ++-- schema/core/nwb.icephys.yaml | 2 +- schema/core/nwb.image.yaml | 6 +----- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/schema/core/nwb.ecephys.yaml b/schema/core/nwb.ecephys.yaml index 82806f31..eea0cfad 100644 --- a/schema/core/nwb.ecephys.yaml +++ b/schema/core/nwb.ecephys.yaml @@ -12,7 +12,7 @@ groups: value: Stores acquired voltage data from extracellular recordings datasets: - name: data - dtype: float + dtype: numeric doc: Recorded voltage data. attributes: - name: unit @@ -50,7 +50,7 @@ groups: value: Snapshots of spike events from data. datasets: - name: data - dtype: float32 + dtype: numeric doc: Spike waveforms. attributes: - name: unit diff --git a/schema/core/nwb.icephys.yaml b/schema/core/nwb.icephys.yaml index 9e73969b..b46c58be 100644 --- a/schema/core/nwb.icephys.yaml +++ b/schema/core/nwb.icephys.yaml @@ -17,7 +17,7 @@ groups: required: false datasets: - name: data - dtype: float + dtype: numeric doc: Recorded voltage or current. dims: - num_times diff --git a/schema/core/nwb.image.yaml b/schema/core/nwb.image.yaml index 1b31be3a..40f1adf9 100644 --- a/schema/core/nwb.image.yaml +++ b/schema/core/nwb.image.yaml @@ -44,12 +44,8 @@ groups: doc: Value is 'Storage object for time-series 2-D image data' value: Storage object for time-series 2-D image data datasets: - - name: bits_per_pixel - dtype: int32 - doc: Number of bit per image pixel. - quantity: '?' - name: data - dtype: int8 + dtype: numeric doc: Either binary data containing image or empty. quantity: '?' dims: From 3a5e45d203c410771a5e1dd02b2fd95b4c1729f6 Mon Sep 17 00:00:00 2001 From: Lawrence Date: Thu, 17 Jan 2019 21:11:44 -0500 Subject: [PATCH 4/7] tests working with new schema. However, elision rules have been broken, so don't expect scripts to work --- +file/Group.m | 91 +++++++++++++----------- +file/fillConstructor.m | 22 +++--- +io/parseGroup.m | 93 +++++++++++++++---------- +tests/+system/ElectricalSeriesIOTest.m | 4 +- +types/+untyped/Set.m | 2 +- 5 files changed, 124 insertions(+), 88 deletions(-) diff --git a/+file/Group.m b/+file/Group.m index 06b73e06..015020cc 100644 --- a/+file/Group.m +++ b/+file/Group.m @@ -145,9 +145,8 @@ obj.hasAnonData = anonDataCnt > 0; obj.hasAnonGroups = anonGroupCnt > 0; - obj.elide = obj.scalar && isempty(obj.type) && isempty(obj.attributes)... - && isempty(obj.links) && ~obj.hasAnonData && ~obj.hasAnonGroups... - && ~obj.defaultEmpty; + obj.elide = ~isempty(obj.name) && obj.scalar && isempty(obj.type) &&... + isempty(obj.attributes); end function props = getProps(obj) @@ -159,42 +158,6 @@ error('getProps shouldn''t be called on a constrained set.'); end - %untyped - % parse props and return. - - %typed - % containersMap of properties -> types - % parse props and return; - - %subgroups - for i=1:length(obj.subgroups) - %if typed, check if constraint - % if constraint, add its type and continue - % otherwise, call getprops and assign to its name. - %if untyped, check if elided - % if elided, add to prefix and check all subgroups, attributes and datasets. - % otherwise, call getprops and assign to its name. - sub = obj.subgroups(i); - if isempty(sub.type) - if sub.elide - subprops = sub.getProps; - epkeys = keys(subprops); - for j=1:length(epkeys) - epk = epkeys{j}; - props([sub.name '_' epk]) = subprops(epk); - end - else - props(sub.name) = sub; - end - else - if isempty(sub.name) - props(lower(sub.type)) = sub; - else - props(sub.name) = sub; - end - end - end - %datasets for i=1:length(obj.datasets) %if typed, check if constraint @@ -230,6 +193,56 @@ props = [props;... containers.Map({obj.links.name}, num2cell(obj.links))]; end + + %untyped + % parse props and return. + + %typed + % containersMap of properties -> types + % parse props and return; + + %subgroups + for i=1:length(obj.subgroups) + %if typed, check if constraint + % if constraint, add its type and continue + % otherwise, call getprops and assign to its name. + %if untyped, check if elided + % if elided, add to prefix and check all subgroups, attributes and datasets. + % otherwise, call getprops and assign to its name. + sub = obj.subgroups(i); + if isempty(sub.type) + if sub.elide + subprops = sub.getProps; + epkeys = keys(subprops); + for j=1:length(epkeys) + epk = epkeys{j}; + epval = subprops(epk); + if (isa(epval, 'file.Group') ||... + isa(epval, 'file.Dataset')) &&... + strcmpi(epk, epval.type) &&... + epval.isConstrainedSet + propname = sub.name; + else + propname = [sub.name '_' epk]; + end + if isKey(props, propname) + keyboard; + props(propname) = {props(propname); subprops(epk)}; + else + props(propname) = subprops(epk); + end + end + else + props(sub.name) = sub; + end + else + if isempty(sub.name) + props(lower(sub.type)) = sub; + else + props(sub.name) = sub; + end + end + end end end end \ No newline at end of file diff --git a/+file/fillConstructor.m b/+file/fillConstructor.m index 48c9eacf..cbb4c76f 100644 --- a/+file/fillConstructor.m +++ b/+file/fillConstructor.m @@ -112,8 +112,8 @@ if isempty(names) return; end - -constrained = false(size(names)); +% if there's a root object that is a constrained set, let it be hoistable from dynamic arguments +dynamicConstrained = false(size(names)); anon = false(size(names)); isattr = false(size(names)); typenames = repmat({''}, size(names)); @@ -123,12 +123,12 @@ prop = props(nm); if isa(prop, 'file.Group') || isa(prop, 'file.Dataset') - constrained(i) = prop.isConstrainedSet; + dynamicConstrained(i) = prop.isConstrainedSet && strcmpi(nm, prop.type); anon(i) = ~prop.isConstrainedSet && isempty(prop.name); if ~isempty(prop.type) pc_namespace = namespace.getNamespace(prop.type); - varnames{i} = prop.type; + varnames{i} = nm; if ~isempty(pc_namespace) typenames{i} = ['types.' pc_namespace.name '.' prop.type]; end @@ -144,7 +144,7 @@ 'the namespace or class definition for type `%1$s` or fix its schema.']; invalid = cellfun('isempty', typenames); -invalidWarn = invalid & (constrained | anon) & ~isattr; +invalidWarn = invalid & (dynamicConstrained | anon) & ~isattr; invalidVars = varnames(invalidWarn); for i=1:length(invalidVars) warning(warnmsg, invalidVars{i}); @@ -155,10 +155,10 @@ deleteFromVars = 'varargin(ivarargin) = [];'; %if constrained/anon sets exist, then check for nonstandard parameters and add as %container.map -constrainedTypes = typenames(constrained & ~invalid); -constrainedVars = varnames(constrained & ~invalid); +constrainedTypes = typenames(dynamicConstrained & ~invalid); +constrainedVars = varnames(dynamicConstrained & ~invalid); methodCalls = strcat('[obj.', constrainedVars, ', ivarargin] = ',... - ' types.util.parseConstrained(obj,''', pname, ''', ''',... + ' types.util.parseConstrained(obj,''', constrainedVars, ''', ''',... constrainedTypes, ''', varargin{:});'); fullBody = cell(length(methodCalls) * 2,1); fullBody(1:2:end) = methodCalls; @@ -185,11 +185,13 @@ 'p.PartialMatching = false;',... 'p.StructExpand = false;'}; -names = names(~constrained & ~anon); +names = names(~dynamicConstrained & ~anon); defaults = cell(size(names)); for i=1:length(names) prop = props(names{i}); - if isa(prop, 'file.Group') && (prop.hasAnonData || prop.hasAnonGroups) + if (isa(prop, 'file.Group') &&... + (prop.hasAnonData || prop.hasAnonGroups || prop.isConstrainedSet)) ||... + (isa(prop, 'file.Dataset') && prop.isConstrainedSet) defaults{i} = 'types.untyped.Set()'; else defaults{i} = '[]'; diff --git a/+io/parseGroup.m b/+io/parseGroup.m index 97798224..91f56e00 100644 --- a/+io/parseGroup.m +++ b/+io/parseGroup.m @@ -52,15 +52,28 @@ parsed(root) = []; end else - %elide group properties - propnames = keys(gprops); - typeprops = setdiff(properties(typename), propnames); - elided_typeprops = typeprops(startsWith(typeprops, propnames)); - for i=1:length(elided_typeprops) - etp = elided_typeprops{i}; - gprops(etp) = elide(etp, gprops); + if gprops.Count > 0 + %elide group properties + propnames = keys(gprops); + typeprops = setdiff(properties(typename), propnames); + elided_typeprops = typeprops(startsWith(typeprops, propnames)); + gprops = [gprops; elide(gprops, elided_typeprops)]; + %remove all properties that are embedded sets (sets within sets) + propnames = keys(gprops); + propvals = values(gprops); + valueSetIdx = cellfun('isclass', propvals, 'types.untyped.Set'); + setNames = propnames(valueSetIdx); + setValues = propvals(valueSetIdx); + for i=1:length(setValues) + nlevel = setValues{i}; + nlevelkeys = keys(nlevel); + deepsetIdx = cellfun('isclass', values(nlevel), 'types.untyped.Set'); + nlevel.delete(nlevelkeys(deepsetIdx)); + if nlevel.Count == 0 + remove(gprops, setNames{i}); %delete this set too if it's empty + end + end end - %construct as kwargs and instantiate object kwargs = io.map2kwargs([attrprops; dsprops; gprops; lprops]); if isempty(root) @@ -72,36 +85,44 @@ end end -function set = elide(propname, elideset) -%given propname and a nested set, elide and return flattened set -set = elideset; -prefix = ''; -while ~strcmp(prefix, propname) - ekeys = keys(set); - found = false; - for i=1:length(ekeys) - ek = ekeys{i}; - if isempty(prefix) - pek = ek; - else - pek = [prefix '_' ek]; +function set = elide(elideset, elided_typeprops, prefix) +%given raw data representation, match to closest property. +% return a typemap of matching typeprops and their prop values to turn into kwargs +% depth first search through the set to construct a possible type prop +set = containers.Map; +if nargin < 3 + prefix = ''; +end +elidekeys = keys(elideset); +elidevals = values(elideset); +constrained = types.untyped.Set(); +if ~isempty(prefix) + potentials = strcat(prefix, '_', elidekeys); +else + potentials = elidekeys; +end +for i=1:length(potentials) + pvar = potentials{i}; + pvalue = elidevals{i}; + if isa(pvalue, 'containers.Map') || isa(pvalue, 'types.untyped.Set') + if isa(elideset, 'containers.Map') + nextSet = elideset(elidekeys{i}); + else % types.untyped.Set + nextSet = elideset.get(elidekeys{i}); end - if startsWith(propname, pek) - if isa(set, 'containers.Map') - set = set(ek); - elseif strcmp(propname, pek) - set = set.get(ek); - else - continue; - end - prefix = pek; - found = true; - break; + leads = startsWith(elided_typeprops, pvar); + if ~any(leads) + %this group probably doesn't have any elided values in it. + continue; end + set = [set; elide(nextSet, elided_typeprops(leads), pvar)]; + elseif any(strcmp(pvar, elided_typeprops)) + set(pvar) = pvalue; + elseif ~isempty(prefix) %attempt to combine into a Set + constrained.set(elidekeys{i}, pvalue); end - if ~found - set = []; - return; - end +end +if constrained.Count > 0 + set(prefix) = constrained; end end \ No newline at end of file diff --git a/+tests/+system/ElectricalSeriesIOTest.m b/+tests/+system/ElectricalSeriesIOTest.m index 8f592f68..e54cb3ce 100644 --- a/+tests/+system/ElectricalSeriesIOTest.m +++ b/+tests/+system/ElectricalSeriesIOTest.m @@ -45,10 +45,10 @@ function addContainer(testCase, file) %#ok etColNames, etTblVal)),... 'description', 'electrodes'); - file.general_extracellular_ephys.set('electrodes',ettable); + file.general_extracellular_ephys_electrodes = ettable; file.general_extracellular_ephys.set(egnm, eg); es = types.core.ElectricalSeries( ... - 'data', int32([0:9;10:19]) .', ... + 'data', [0:9;10:19] .', ... 'timestamps', (0:9) .', ... 'electrodes', ... types.core.DynamicTableRegion(... diff --git a/+types/+untyped/Set.m b/+types/+untyped/Set.m index 155d9e4c..09d4181d 100644 --- a/+types/+untyped/Set.m +++ b/+types/+untyped/Set.m @@ -115,7 +115,7 @@ function validateAll(obj) end function obj = delete(obj, name) - obj.map(name) = []; + remove(obj.map, name); end function obj = clear(obj) From 08caa3fd9d8136c14f23f1c40898f882899f499e Mon Sep 17 00:00:00 2001 From: Lawrence Date: Thu, 17 Jan 2019 21:37:33 -0500 Subject: [PATCH 5/7] added new dynamic table test for dynamic column testing --- +file/Group.m | 2 ++ +tests/+system/DynamicTableTest.m | 36 +++++++++++++++++++++++++++++++ +tests/+system/UnitTimesIOTest.m | 4 ++-- 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 +tests/+system/DynamicTableTest.m diff --git a/+file/Group.m b/+file/Group.m index 015020cc..bfbccd07 100644 --- a/+file/Group.m +++ b/+file/Group.m @@ -217,6 +217,8 @@ for j=1:length(epkeys) epk = epkeys{j}; epval = subprops(epk); + % hoist constrained sets to the current + % subname. if (isa(epval, 'file.Group') ||... isa(epval, 'file.Dataset')) &&... strcmpi(epk, epval.type) &&... diff --git a/+tests/+system/DynamicTableTest.m b/+tests/+system/DynamicTableTest.m new file mode 100644 index 00000000..f5a1c1d8 --- /dev/null +++ b/+tests/+system/DynamicTableTest.m @@ -0,0 +1,36 @@ +classdef DynamicTableTest < tests.system.RoundTripTest + methods + function addContainer(~, file) + start_time = types.core.VectorData(... + 'description', 'start_time',... + 'data', 1:100); + stop_time = types.core.VectorData(... + 'description', 'stop_time',... + 'data', 2:200); + colnames = {'start_time', 'stop_time', 'randomvalues'}; + id = types.core.ElementIdentifiers(... + 'data', 1:100); + + randcol = types.core.VectorData(... + 'description', 'random data to be indexed into',... + 'data', rand(500,1)); + randidx = types.core.VectorIndex(... + 'target', types.untyped.ObjectView('/intervals/trials/randomvalues'),... + 'data', 5:5:500 - 1); + + file.intervals_trials = types.core.TimeIntervals(... + 'description', 'test dynamic table columns',... + 'id', id,... + 'start_time', start_time,... + 'stop_time', stop_time,... + 'colnames', colnames,... + 'randomvalues', randcol,... + 'randomvalues_index', randidx); + end + + function c = getContainer(~, file) + c = file.intervals_trials.vectordata.get('randomvalues'); + end + end +end + diff --git a/+tests/+system/UnitTimesIOTest.m b/+tests/+system/UnitTimesIOTest.m index 4e2f82a0..2c713599 100644 --- a/+tests/+system/UnitTimesIOTest.m +++ b/+tests/+system/UnitTimesIOTest.m @@ -14,7 +14,7 @@ function addContainer(~, file) 'target', types.untyped.ObjectView(spike_loc)); ei = types.core.ElementIdentifiers('data', 1:3); file.units = types.core.Units(... - 'colnames', 'id',... + 'colnames', {'spike_times'},... 'description', 'test Units',... 'spike_times', vd, ... 'spike_times_index', vi,... @@ -22,7 +22,7 @@ function addContainer(~, file) end function c = getContainer(~, file) - c = file.units; + c = file.units.spike_times; end end end \ No newline at end of file From 9e08349eede82edf952dab18ce462a5847472924 Mon Sep 17 00:00:00 2001 From: bendichter Date: Fri, 18 Jan 2019 19:52:39 +0200 Subject: [PATCH 6/7] update pynwb tests --- +tests/+system/ElectricalSeriesIOTest.m | 11 - +tests/+system/ImagingPlaneIOTest.m | 4 +- +tests/+system/PhotonSeriesIOTest.m | 6 +- +tests/+system/PyNWBIOTest.m | 4 +- +tests/+system/PyNWBIOTest.py | 287 +++++++++++++----------- +tests/+system/TimeSeriesIOTest.m | 2 +- 6 files changed, 164 insertions(+), 150 deletions(-) diff --git a/+tests/+system/ElectricalSeriesIOTest.m b/+tests/+system/ElectricalSeriesIOTest.m index e54cb3ce..0d517692 100644 --- a/+tests/+system/ElectricalSeriesIOTest.m +++ b/+tests/+system/ElectricalSeriesIOTest.m @@ -1,15 +1,4 @@ classdef ElectricalSeriesIOTest < tests.system.PyNWBIOTest - methods(Test) - function testOutToPyNWB(testCase) - testCase.assumeFail(['Current schema in MatNWB does not include a ElectrodeTable class used by Python tests. ', ... - 'When it does, addContainer in this test will need to be updated to match the Python test']); - end - - function testInFromPyNWB(testCase) - testCase.assumeFail(['Current schema in MatNWB does not include a ElectrodeTable class used by Python tests. ', ... - 'When it does, addContainer in this test will need to be updated to match the Python test']); - end - end methods function addContainer(testCase, file) %#ok diff --git a/+tests/+system/ImagingPlaneIOTest.m b/+tests/+system/ImagingPlaneIOTest.m index fd7835b8..773bdc6c 100644 --- a/+tests/+system/ImagingPlaneIOTest.m +++ b/+tests/+system/ImagingPlaneIOTest.m @@ -12,9 +12,7 @@ function addContainer(testCase, file) %#ok 'excitation_lambda', 6.28, ... 'imaging_rate', 2.718, ... 'indicator', 'GFP', ... - 'location', 'somewhere in the brain',... - 'manifold', zeros(3,3,3),... - 'reference_frame', 'manifold reference'); + 'location', 'somewhere in the brain'); file.general_devices.set('imaging_device_1', dev); file.general_optophysiology.set('imgpln1', ip); end diff --git a/+tests/+system/PhotonSeriesIOTest.m b/+tests/+system/PhotonSeriesIOTest.m index f56a13de..abebb28c 100644 --- a/+tests/+system/PhotonSeriesIOTest.m +++ b/+tests/+system/PhotonSeriesIOTest.m @@ -13,12 +13,10 @@ function addContainer(testCase, file) %#ok 'excitation_lambda', 6.28, ... 'imaging_rate', 2.718, ... 'indicator', 'GFP', ... - 'location', 'somewhere in the brain',... - 'manifold', zeros(3,1),... - 'reference_frame', 'manifold ref'); + 'location', 'somewhere in the brain'); tps = types.core.TwoPhotonSeries( ... - 'data', int32([0:9;10:19]) .', ... + 'data', ones(3,3,3), ... 'imaging_plane', types.untyped.SoftLink('/general/optophysiology/imgpln1'), ... 'data_unit', 'image_unit', ... 'format', 'raw', ... diff --git a/+tests/+system/PyNWBIOTest.m b/+tests/+system/PyNWBIOTest.m index 577d088e..09dd4b8b 100644 --- a/+tests/+system/PyNWBIOTest.m +++ b/+tests/+system/PyNWBIOTest.m @@ -31,8 +31,8 @@ function testInFromPyNWB(testCase) methods function [status, cmdout] = runPyTest(testCase, testName) - setenv('PYTHONPATH', fileparts(mfilename('fullpath'))); - cmd = sprintf('python -B -m unittest %s.%s.%s', 'PyNWBIOTest', testCase.className(), testName); + %setenv('PYTHONPATH', fileparts(mfilename('fullpath'))); + cmd = sprintf('/Users/bendichter/anaconda3/bin/python -B -m unittest %s.%s.%s', 'PyNWBIOTest', testCase.className(), testName); [status, cmdout] = system(cmd); end end diff --git a/+tests/+system/PyNWBIOTest.py b/+tests/+system/PyNWBIOTest.py index 5629ad46..5323bf0f 100644 --- a/+tests/+system/PyNWBIOTest.py +++ b/+tests/+system/PyNWBIOTest.py @@ -2,153 +2,182 @@ from datetime import datetime import os.path import numpy as np +from dateutil.tz import tzlocal +import numpy.testing as npt from pynwb import NWBContainer, get_manager, NWBFile, NWBData, TimeSeries -from pynwb.ecephys import Device, ElectricalSeries, ElectrodeGroup, ElectrodeTable, ElectrodeTableRegion, Clustering -from pynwb.ophys import ImagingPlane, OpticalChannel, TwoPhotonSeries +from pynwb.ecephys import ElectricalSeries, Clustering +from pynwb.ophys import OpticalChannel, TwoPhotonSeries from pynwb.form.backends.hdf5 import HDF5IO class PyNWBIOTest(unittest.TestCase): - def setUp(self): - start_time = datetime(1970, 1, 1, 12, 0, 0) - create_date = datetime(2017, 4, 15, 12, 0, 0) - self.__file = NWBFile('a test source', 'a test NWB File', 'TEST123', start_time, file_create_date=create_date) - self.__container = self.addContainer(self.file) - - @property - def file(self): - return self.__file - - @property - def container(self): - return self.__container - - def testInFromMatNWB(self): - filename = 'MatNWB.' + self.__class__.__name__ + '.testOutToPyNWB.nwb' - io = HDF5IO(filename, manager=get_manager()) - matfile = io.read() - io.close() - matcontainer = self.getContainer(matfile) - pycontainer = self.getContainer(self.file) - self.assertContainerEqual(matcontainer, pycontainer) - - def testOutToMatNWB(self): - filename = 'PyNWB.' + self.__class__.__name__ + '.testOutToMatNWB.nwb' - io = HDF5IO(filename, manager=get_manager()) - io.write(self.file) - io.close() - self.assertTrue(os.path.isfile(filename)) - - def addContainer(self, file): - raise unittest.SkipTest('Cannot run test unless addContainer is implemented') - - def getContainer(self, file): - raise unittest.SkipTest('Cannot run test unless getContainer is implemented') - - def assertContainerEqual(self, container1, container2): - type1 = type(container1) - type2 = type(container2) - self.assertEqual(type1, type2) - for nwbfield in container1.__nwbfields__: - with self.subTest(nwbfield=nwbfield, container_type=type1.__name__): - f1 = getattr(container1, nwbfield) - f2 = getattr(container2, nwbfield) - if isinstance(f1, (tuple, list, np.ndarray)): - if len(f1) > 0 and isinstance(f1[0], NWBContainer): - for sub1, sub2 in zip(f1, f2): - self.assertContainerEqual(sub1, sub2) - continue - else: - self.assertTrue(np.array_equal(f1, f2)) - elif isinstance(f1, dict) and len(f1) and isinstance(next(iter(f1.values())), NWBContainer): - f1_keys = set(f1.keys()) - f2_keys = set(f2.keys()) - self.assertSetEqual(f1_keys, f2_keys) - for k in f1_keys: - with self.subTest(module_name=k): - self.assertContainerEqual(f1[k], f2[k]) - elif isinstance(f1, NWBContainer): - self.assertContainerEqual(f1, f2) - elif isinstance(f1, NWBData): - self.assertDataEqual(f1, f2) - else: - self.assertEqual(f1, f2) - - def assertDataEqual(self, data1, data2): - self.assertEqual(type(data1), type(data2)) - self.assertEqual(len(data1), len(data2)) + def setUp(self): + start_time = datetime(1970, 1, 1, 12, 0, 0, tzinfo=tzlocal()) + create_date = datetime(2017, 4, 15, 12, 0, 0, tzinfo=tzlocal()) + self.__file = NWBFile('a test NWB File', 'TEST123', start_time, file_create_date=create_date) + self.__container = self.addContainer(self.file) + + @property + def file(self): + return self.__file + + @property + def container(self): + return self.__container + + def testInFromMatNWB(self): + filename = 'MatNWB.' + self.__class__.__name__ + '.testOutToPyNWB.nwb' + with HDF5IO(filename, manager=get_manager(), mode='r') as io: + matfile = io.read() + matcontainer = self.getContainer(matfile) + pycontainer = self.getContainer(self.file) + self.assertContainerEqual(matcontainer, pycontainer) + + def testOutToMatNWB(self): + filename = 'PyNWB.' + self.__class__.__name__ + '.testOutToMatNWB.nwb' + with HDF5IO(filename, manager=get_manager(), mode='w') as io: + io.write(self.file) + self.assertTrue(os.path.isfile(filename)) + + def addContainer(self, file): + raise unittest.SkipTest('Cannot run test unless addContainer is implemented') + + def getContainer(self, file): + raise unittest.SkipTest('Cannot run test unless getContainer is implemented') + + def assertContainerEqual(self, container1, container2): # noqa: C901 + ''' + container1 is what was read or generated + container2 is what is hardcoded in the TestCase + ''' + type1 = type(container1) + type2 = type(container2) + self.assertEqual(type1, type2) + for nwbfield in container1.__nwbfields__: + with self.subTest(nwbfield=nwbfield, container_type=type1.__name__): + f1 = getattr(container1, nwbfield) + f2 = getattr(container2, nwbfield) + if isinstance(f1, (tuple, list, np.ndarray)): + if len(f1) > 0: + if isinstance(f1[0], NWBContainer): + for sub1, sub2 in zip(f1, f2): + self.assertContainerEqual(sub1, sub2) + elif isinstance(f1[0], NWBData): + for sub1, sub2 in zip(f1, f2): + self.assertDataEqual(sub1, sub2) + continue + else: + self.assertEqual(len(f1), len(f2)) + if len(f1) == 0: + continue + if isinstance(f1[0], float): + for v1, v2 in zip(f1, f2): + self.assertAlmostEqual(v1, v2, places=6) + else: + self.assertTrue(np.array_equal(f1, f2)) + elif isinstance(f1, dict) and len(f1) and isinstance(next(iter(f1.values())), NWBContainer): + f1_keys = set(f1.keys()) + f2_keys = set(f2.keys()) + self.assertSetEqual(f1_keys, f2_keys) + for k in f1_keys: + with self.subTest(module_name=k): + self.assertContainerEqual(f1[k], f2[k]) + elif isinstance(f1, NWBContainer): + self.assertContainerEqual(f1, f2) + elif isinstance(f1, NWBData) or isinstance(f2, NWBData): + if isinstance(f1, NWBData) and isinstance(f2, NWBData): + self.assertDataEqual(f1, f2) + elif isinstance(f1, NWBData): + self.assertTrue(np.array_equal(f1.data, f2)) + elif isinstance(f2, NWBData): + self.assertTrue(np.array_equal(f1.data, f2)) + else: + if isinstance(f1, (float, np.float32, np.float16)): + npt.assert_almost_equal(f1, f2) + else: + self.assertEqual(f1, f2) + + def assertDataEqual(self, data1, data2): + self.assertEqual(type(data1), type(data2)) + self.assertEqual(len(data1), len(data2)) + class TimeSeriesIOTest(PyNWBIOTest): - def addContainer(self, file): - ts = TimeSeries('test_timeseries', 'example_source', list(range(100, 200, 10)), - 'SIunit', timestamps=list(range(10)), resolution=0.1) - file.add_acquisition(ts) - return ts + def addContainer(self, file): + ts = TimeSeries('test_timeseries', list(range(100, 200, 10)), + 'SIunit', timestamps=np.arange(10, dtype=float), resolution=0.1) + file.add_acquisition(ts) + return ts + + def getContainer(self, file): + return file.get_acquisition(self.container.name) - def getContainer(self, file): - return file.get_acquisition(self.container.name) class ElectrodeGroupIOTest(PyNWBIOTest): - def addContainer(self, file): - dev1 = file.create_device('dev1', 'a test source') - eg = file.create_electrode_group('elec1', 'a test source', 'a test ElectrodeGroup', 'a nonexistent place', dev1) - return eg + def addContainer(self, file): + dev1 = file.create_device('dev1', 'dev1 description') + eg = file.create_electrode_group('elec1', 'a test ElectrodeGroup', 'a nonexistent place', dev1) + return eg + + def getContainer(self, file): + return file.get_electrode_group(self.container.name) - def getContainer(self, file): - return file.get_electrode_group(self.container.name) class ElectricalSeriesIOTest(PyNWBIOTest): - def addContainer(self, file): - dev1 = file.create_device('dev1', 'a test source') - group = file.create_electrode_group('tetrode1', 'a test source', 'tetrode description', 'tetrode location', dev1) - for i in range(4): - file.add_electrode(i+1, 1.0, 2.0, 3.0, 1.0, 'CA1', 'none', 'first channel of tetrode', group) - region = ElectrodeTableRegion(table, [0, 2], 'the first and third electrodes') # noqa: F405 - data = list(zip(range(10), range(10, 20))) - timestamps = list(range(10)) - es = ElectricalSeries('test_eS', 'a hypothetical source', data, region, timestamps=timestamps) - file.add_acquisition(es) - return es - - def getContainer(self, file): - return file.get_acquisition(self.container.name) + def addContainer(self, file): + dev1 = file.create_device('dev1', 'dev1 description') + group = file.create_electrode_group('tetrode1', 'tetrode description', 'tetrode location', dev1) + for i in range(4): + file.add_electrode(1.0, 2.0, 3.0, 1.0, 'CA1', 'none', group) + region = file.create_electrode_table_region([0, 2], 'the first and third electrodes') + data = list(zip(range(10), range(10, 20))) + timestamps = list(range(10)) + es = ElectricalSeries('test_eS', data, region, timestamps=timestamps) + file.add_acquisition(es) + return es + + def getContainer(self, file): + return file.get_acquisition(self.container.name) + class ImagingPlaneIOTest(PyNWBIOTest): - def addContainer(self, file): - oc = OpticalChannel('optchan1', 'unit test TestImagingPlaneIO', 'a fake OpticalChannel', 3.14) - ip = file.create_imaging_plane('imgpln1', 'unit test TestImagingPlaneIO', oc, 'a fake ImagingPlane', - 'imaging_device_1', 6.28, '2.718', 'GFP', 'somewhere in the brain') - return ip + def addContainer(self, file): + dev1 = file.create_device('imaging_device_1', 'dev1 description') + oc = OpticalChannel('optchan1', 'a fake OpticalChannel', 3.14) + ip = file.create_imaging_plane('imgpln1', oc, 'a fake ImagingPlane', + dev1, 6.28, 2.718, 'GFP', 'somewhere in the brain') + return ip + + def getContainer(self, file): + return file.get_imaging_plane(self.container.name) - def getContainer(self, file): - return file.get_imaging_plane(self.container.name) class PhotonSeriesIOTest(PyNWBIOTest): - def addContainer(self, file): - oc = OpticalChannel('optchan1', 'unit test TestImagingPlaneIO', 'a fake OpticalChannel', 3.14) - ip = file.create_imaging_plane('imgpln1', 'unit test TestImagingPlaneIO', oc, - 'a fake ImagingPlane', 'imaging_device_1', 6.28, '2.718', - 'GFP', 'somewhere in the brain') - data = list(zip(range(10), range(10, 20))) - timestamps = list(range(10)) - fov = [2.0, 2.0, 5.0] - tps = TwoPhotonSeries('test_2ps', 'unit test TestTwoPhotonSeries', ip, data, - 'image_unit', 'raw', fov, 1.7, 3.4, timestamps=timestamps, dimension=[200, 200]) - file.add_acquisition(tps) - - def getContainer(self, file): - return file.get_acquisition(self.container.name) + def addContainer(self, file): + dev1 = file.create_device('dev1', 'dev1 description') + oc = OpticalChannel('optchan1', 'unit test TestImagingPlaneIO', 3.14) + ip = file.create_imaging_plane('imgpln1', oc, 'a fake ImagingPlane', + dev1, 6.28, 2.718, 'GFP', 'somewhere in the brain') + data = np.ones((3, 3, 3)) + timestamps = list(range(10)) + fov = [2.0, 2.0, 5.0] + tps = TwoPhotonSeries('test_2ps', ip, data, 'image_unit', 'raw', + fov, 1.7, 3.4, timestamps=timestamps, dimension=[200, 200]) + file.add_acquisition(tps) + + def getContainer(self, file): + return file.get_acquisition(self.container.name) + class NWBFileIOTest(PyNWBIOTest): - def addContainer(self, file): - ts = TimeSeries('test_timeseries', 'example_source', list(range(100, 200, 10)), - 'SIunit', timestamps=list(range(10)), resolution=0.1) - self.file.add_acquisition(ts) - mod = file.create_processing_module('test_module', 'a test source for a ProcessingModule', 'a test module') - mod.add_container(Clustering("an example source for Clustering", - "A fake Clustering interface", [0, 1, 2, 0, 1, 2], - [100, 101, 102], list(range(10, 61, 10)))) - - def getContainer(self, file): - return file + def addContainer(self, file): + ts = TimeSeries('test_timeseries', list(range(100, 200, 10)), + 'SIunit', timestamps=list(range(10)), resolution=0.1) + self.file.add_acquisition(ts) + mod = file.create_processing_module('test_module', 'a test module') + mod.add_container(Clustering("A fake Clustering interface", [0, 1, 2, 0, 1, 2], + [100., 101., 102.], list(range(10, 61, 10)))) + + def getContainer(self, file): + return file diff --git a/+tests/+system/TimeSeriesIOTest.m b/+tests/+system/TimeSeriesIOTest.m index 87b010dc..401e82f4 100644 --- a/+tests/+system/TimeSeriesIOTest.m +++ b/+tests/+system/TimeSeriesIOTest.m @@ -2,7 +2,7 @@ methods function addContainer(testCase, file) %#ok ts = types.core.TimeSeries(... - 'data', int32(100:10:190) .', ... + 'data', (100:10:190) .', ... 'data_unit', 'SIunit', ... 'timestamps', (0:9) .', ... 'data_resolution', 0.1); From 634dac92b9be84470cd4183eb650b068cac04376 Mon Sep 17 00:00:00 2001 From: Lawrence Date: Fri, 18 Jan 2019 15:01:11 -0500 Subject: [PATCH 7/7] fixed some elision confusion when reading from file. --- +io/parseGroup.m | 69 ++++++++++++++++++------------------------- +types/+untyped/Set.m | 2 +- 2 files changed, 30 insertions(+), 41 deletions(-) diff --git a/+io/parseGroup.m b/+io/parseGroup.m index 91f56e00..da6d463c 100644 --- a/+io/parseGroup.m +++ b/+io/parseGroup.m @@ -54,25 +54,8 @@ else if gprops.Count > 0 %elide group properties - propnames = keys(gprops); - typeprops = setdiff(properties(typename), propnames); - elided_typeprops = typeprops(startsWith(typeprops, propnames)); - gprops = [gprops; elide(gprops, elided_typeprops)]; - %remove all properties that are embedded sets (sets within sets) - propnames = keys(gprops); - propvals = values(gprops); - valueSetIdx = cellfun('isclass', propvals, 'types.untyped.Set'); - setNames = propnames(valueSetIdx); - setValues = propvals(valueSetIdx); - for i=1:length(setValues) - nlevel = setValues{i}; - nlevelkeys = keys(nlevel); - deepsetIdx = cellfun('isclass', values(nlevel), 'types.untyped.Set'); - nlevel.delete(nlevelkeys(deepsetIdx)); - if nlevel.Count == 0 - remove(gprops, setNames{i}); %delete this set too if it's empty - end - end + elided_gprops = elide(gprops, properties(typename)); + gprops = [gprops; elided_gprops]; end %construct as kwargs and instantiate object kwargs = io.map2kwargs([attrprops; dsprops; gprops; lprops]); @@ -85,17 +68,18 @@ end end -function set = elide(elideset, elided_typeprops, prefix) +%NOTE: SIDE EFFECTS ALTER THE SET +function elided = elide(set, prop, prefix) %given raw data representation, match to closest property. % return a typemap of matching typeprops and their prop values to turn into kwargs % depth first search through the set to construct a possible type prop -set = containers.Map; if nargin < 3 prefix = ''; end -elidekeys = keys(elideset); -elidevals = values(elideset); -constrained = types.untyped.Set(); +elided = containers.Map; +elidekeys = keys(set); +elidevals = values(set); +drop = false(size(elidekeys)); if ~isempty(prefix) potentials = strcat(prefix, '_', elidekeys); else @@ -105,24 +89,29 @@ pvar = potentials{i}; pvalue = elidevals{i}; if isa(pvalue, 'containers.Map') || isa(pvalue, 'types.untyped.Set') - if isa(elideset, 'containers.Map') - nextSet = elideset(elidekeys{i}); - else % types.untyped.Set - nextSet = elideset.get(elidekeys{i}); + if pvalue.Count == 0 + drop(i) = true; + continue; %delete end - leads = startsWith(elided_typeprops, pvar); - if ~any(leads) - %this group probably doesn't have any elided values in it. - continue; + leads = startsWith(prop, pvar); + if any(leads) + %since set has been edited, we bubble up deletion of the old keys. + subset = elide(pvalue, prop(leads), pvar); + elided = [elided; subset]; + if pvalue.Count == 0 + drop(i) = true; + elseif any(strcmp(pvar, prop)) + elided(pvar) = pvalue; + drop(i) = true; + else + warning('Unable to match property `%s` under prefix `%s`',... + pvar, prefix); + end end - set = [set; elide(nextSet, elided_typeprops(leads), pvar)]; - elseif any(strcmp(pvar, elided_typeprops)) - set(pvar) = pvalue; - elseif ~isempty(prefix) %attempt to combine into a Set - constrained.set(elidekeys{i}, pvalue); + elseif any(strcmp(pvar, prop)) + elided(pvar) = pvalue; + drop(i) = true; end end -if constrained.Count > 0 - set(prefix) = constrained; -end +remove(set, elidekeys(drop)); %delete all leftovers that were yielded end \ No newline at end of file diff --git a/+types/+untyped/Set.m b/+types/+untyped/Set.m index 09d4181d..9f3ff95c 100644 --- a/+types/+untyped/Set.m +++ b/+types/+untyped/Set.m @@ -114,7 +114,7 @@ function validateAll(obj) end end - function obj = delete(obj, name) + function obj = remove(obj, name) remove(obj.map, name); end