From 56dd8b954888e2343ca938da3b72dadb222bb165 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Sun, 20 Jan 2019 18:04:03 +0200 Subject: [PATCH 001/108] Moved Signals vis functions from cortexlab directory --- cortexlab/+vis/checker4.m | 126 ---------------------------------- cortexlab/+vis/checker5.m | 126 ---------------------------------- cortexlab/+vis/checker6.m | 126 ---------------------------------- cortexlab/+vis/checkerLeft.m | 126 ---------------------------------- cortexlab/+vis/checkerRight.m | 126 ---------------------------------- signals | 2 +- 6 files changed, 1 insertion(+), 631 deletions(-) delete mode 100644 cortexlab/+vis/checker4.m delete mode 100644 cortexlab/+vis/checker5.m delete mode 100644 cortexlab/+vis/checker6.m delete mode 100644 cortexlab/+vis/checkerLeft.m delete mode 100644 cortexlab/+vis/checkerRight.m diff --git a/cortexlab/+vis/checker4.m b/cortexlab/+vis/checker4.m deleted file mode 100644 index be837609..00000000 --- a/cortexlab/+vis/checker4.m +++ /dev/null @@ -1,126 +0,0 @@ -function elem = checker3(t) -%vis.checker A grid of rectangles -% Detailed explanation goes here - -elem = t.Node.Net.subscriptableOrigin('checker'); - -%% make initial layers to be used as templates -maskTemplate = vis.emptyLayer(); -maskTemplate.isPeriodic = false; -maskTemplate.interpolation = 'nearest'; -maskTemplate.show = true; -maskTemplate.colourMask = [false false false true]; - -maskTemplate.textureId = 'checkerMaskPixel'; -[maskTemplate.rgba, maskTemplate.rgbaSize] = vis.rgba(0, 0); -maskTemplate.blending = '1-source'; % allows us to lay down our zero alpha value - -stencilTemplate = maskTemplate; -stencilTemplate.textureId = 'checkerStencilPixel'; -[stencilTemplate.rgba, stencilTemplate.rgbaSize] = vis.rgba(1, 1); -stencilTemplate.blending = 'none'; - -% pattern layer uses the alpha values laid down by mask layers -patternLayer = vis.emptyLayer(); -patternLayer.textureId = sprintf('~checker%i', randi(2^32)); -patternLayer.isPeriodic = false; -patternLayer.interpolation = 'nearest'; -patternLayer.blending = 'destination'; % use the alpha mask gets laid down before this - -%% construct signals used to assemble layers -% N rows by cols signal is derived from the size of the pattern array but -% we skip repeats so that pattern changes don't update the mask layers -% unless the size has acutally changed -nRowsByCols = elem.pattern.flatten().map(@size).skipRepeats(); -aziRange = elem.azimuthRange.flatten(); -altRange = elem.altitudeRange.flatten(); -sizeFrac = elem.rectSizeFrac.flatten(); -% signal containing the masking layers -gridMaskLayers = mapn(nRowsByCols, aziRange, altRange, sizeFrac, ... - maskTemplate, stencilTemplate, @gridMask); -% signal contain the checker layer -checkerLayer = scan(elem.pattern.flatten(), @updatePattern,... - elem.colour.flatten(), @updateColour,... - elem.azimuthRange.flatten(), @updateAzi,... - elem.altitudeRange.flatten(), @updateAlt,... - elem.show.flatten(), @updateShow,... - patternLayer); % initial value -%% set default attribute values -elem.layers = [gridMaskLayers checkerLayer]; -elem.azimuthRange = [-132 132]; -elem.altitudeRange = [-36 36]; -elem.rectSizeFrac = [1 1]; % horizontal and vertical size of each rectangle -elem.pattern = [ - 1 -1 1 -1 - -1 0 0 0 - 1 0 0 0 - -1 1 -1 1]; - elem.show = true; -end - -%% helper functions -function layer = updatePattern(layer, pattern) -% map pattern from -1 -> 1 range to 0->255, cast to 8 bit integers, then -% convert to RGBA texture format. -[layer.rgba, layer.rgbaSize] = vis.rgbaFromUint8(uint8(127.5*(1 + pattern)), 1); -end - -function layer = updateColour(layer, colour) -layer.maxColour = [colour 1]; -end - -function layer = updateAzi(layer, aziRange) -layer.size(1) = abs(diff(aziRange)); -layer.texOffset(1) = mean(aziRange); -end - -function layer = updateAlt(layer, altRange) -layer.size(2) = abs(diff(altRange)); -layer.texOffset(2) = mean(altRange); -end - -function layer = updateShow(layer, show) -layer.show = show; -end - -function layers = gridMask(nRowsByCols, aziRange, altRange, sizeFrac, mask, stencil) -gridDims = [abs(diff(aziRange)) abs(diff(altRange))]; -cellSize = gridDims./flip(nRowsByCols); -nCols = nRowsByCols(2) + 1; -nRows = nRowsByCols(1) + 1; -midAzi = mean(aziRange); -midAlt = mean(altRange); -%% base layer to imprint area the checker can draw on (by applying an alpha mask) -stencil.texOffset = [midAzi midAlt]; -stencil.size = gridDims; -if any(sizeFrac < 1) - %% layers for lines making up mask grid - masks out margins around each square - % make layers for vertical lines - if nCols > 1 - azi = linspace(aziRange(1), aziRange(2), nCols); - else - azi = midAzi; - end - collayers = repmat(mask, 1, nCols); - for vi = 1:nCols - collayers(vi).texOffset = [azi(vi) midAlt]; - end - [collayers.size] = deal([(1 - sizeFrac(1))*cellSize(1) gridDims(2)]); - % make layers for horizontal lines - if nRows > 1 - alt = linspace(altRange(1), altRange(2), nRows); - else - alt = midAlt; - end - rowlayers = repmat(mask, 1, nRows); - for hi = 1:nRows - rowlayers(hi).texOffset = [midAzi alt(hi)]; - end - [rowlayers.size] = deal([gridDims(1) (1 - sizeFrac(2))*cellSize(2)]); - %% combine the layers and return - layers = [stencil collayers rowlayers]; -else % no mask grid needed as each cell is full size - layers = stencil; -end - -end \ No newline at end of file diff --git a/cortexlab/+vis/checker5.m b/cortexlab/+vis/checker5.m deleted file mode 100644 index be37a1f4..00000000 --- a/cortexlab/+vis/checker5.m +++ /dev/null @@ -1,126 +0,0 @@ -function elem = checker5(t) -%vis.checker A grid of rectangles -% Detailed explanation goes here - -elem = t.Node.Net.subscriptableOrigin('checker'); - -%% make initial layers to be used as templates -maskTemplate = vis.emptyLayer(); -maskTemplate.isPeriodic = false; -maskTemplate.interpolation = 'nearest'; -maskTemplate.show = true; -maskTemplate.colourMask = [false false false true]; - -maskTemplate.textureId = 'checkerMaskPixel'; -[maskTemplate.rgba, maskTemplate.rgbaSize] = vis.rgba(0, 0); -maskTemplate.blending = '1-source'; % allows us to lay down our zero alpha value - -stencilTemplate = maskTemplate; -stencilTemplate.textureId = 'checkerStencilPixel'; -[stencilTemplate.rgba, stencilTemplate.rgbaSize] = vis.rgba(1, 1); -stencilTemplate.blending = 'none'; - -% pattern layer uses the alpha values laid down by mask layers -patternLayer = vis.emptyLayer(); -patternLayer.textureId = sprintf('~checker%i', randi(2^32)); -patternLayer.isPeriodic = false; -patternLayer.interpolation = 'nearest'; -patternLayer.blending = 'destination'; % use the alpha mask gets laid down before this - -%% construct signals used to assemble layers -% N rows by cols signal is derived from the size of the pattern array but -% we skip repeats so that pattern changes don't update the mask layers -% unless the size has acutally changed -nRowsByCols = elem.pattern.flatten().map(@size).skipRepeats(); -aziRange = elem.azimuthRange.flatten(); -altRange = elem.altitudeRange.flatten(); -sizeFrac = elem.rectSizeFrac.flatten(); -% signal containing the masking layers -gridMaskLayers = mapn(nRowsByCols, aziRange, altRange, sizeFrac, ... - maskTemplate, stencilTemplate, @gridMask); -% signal contain the checker layer -checkerLayer = scan(elem.pattern.flatten(), @updatePattern,... - elem.colour.flatten(), @updateColour,... - elem.azimuthRange.flatten(), @updateAzi,... - elem.altitudeRange.flatten(), @updateAlt,... - elem.show.flatten(), @updateShow,... - patternLayer); % initial value -%% set default attribute values -elem.layers = [gridMaskLayers checkerLayer]; -elem.azimuthRange = [-132 132]; -elem.altitudeRange = [-36 36]; -elem.rectSizeFrac = [1 1]; % horizontal and vertical size of each rectangle -elem.pattern = [ - 1 -1 1 -1 - -1 0 0 0 - 1 0 0 0 - -1 1 -1 1]; - elem.show = true; -end - -%% helper functions -function layer = updatePattern(layer, pattern) -% map pattern from -1 -> 1 range to 0->255, cast to 8 bit integers, then -% convert to RGBA texture format. -[layer.rgba, layer.rgbaSize] = vis.rgbaFromUint8_PC(uint8(127.5*(1 + pattern)), 1); -end - -function layer = updateColour(layer, colour) -layer.maxColour = [colour 1]; -end - -function layer = updateAzi(layer, aziRange) -layer.size(1) = abs(diff(aziRange)); -layer.texOffset(1) = mean(aziRange); -end - -function layer = updateAlt(layer, altRange) -layer.size(2) = abs(diff(altRange)); -layer.texOffset(2) = mean(altRange); -end - -function layer = updateShow(layer, show) -layer.show = show; -end - -function layers = gridMask(nRowsByCols, aziRange, altRange, sizeFrac, mask, stencil) -gridDims = [abs(diff(aziRange)) abs(diff(altRange))]; -cellSize = gridDims./fliplr(nRowsByCols); -nCols = nRowsByCols(2) + 1; -nRows = nRowsByCols(1) + 1; -midAzi = mean(aziRange); -midAlt = mean(altRange); -%% base layer to imprint area the checker can draw on (by applying an alpha mask) -stencil.texOffset = [midAzi midAlt]; -stencil.size = gridDims; -if any(sizeFrac < 1) - %% layers for lines making up mask grid - masks out margins around each square - % make layers for vertical lines - if nCols > 1 - azi = linspace(aziRange(1), aziRange(2), nCols); - else - azi = midAzi; - end - collayers = repmat(mask, 1, nCols); - for vi = 1:nCols - collayers(vi).texOffset = [azi(vi) midAlt]; - end - [collayers.size] = deal([(1 - sizeFrac(1))*cellSize(1) gridDims(2)]); - % make layers for horizontal lines - if nRows > 1 - alt = linspace(altRange(1), altRange(2), nRows); - else - alt = midAlt; - end - rowlayers = repmat(mask, 1, nRows); - for hi = 1:nRows - rowlayers(hi).texOffset = [midAzi alt(hi)]; - end - [rowlayers.size] = deal([gridDims(1) (1 - sizeFrac(2))*cellSize(2)]); - %% combine the layers and return - layers = [stencil collayers rowlayers]; -else % no mask grid needed as each cell is full size - layers = stencil; -end - -end \ No newline at end of file diff --git a/cortexlab/+vis/checker6.m b/cortexlab/+vis/checker6.m deleted file mode 100644 index 1733d8f7..00000000 --- a/cortexlab/+vis/checker6.m +++ /dev/null @@ -1,126 +0,0 @@ -function elem = checker6(t) -%vis.checker A grid of rectangles -% Detailed explanation goes here - -elem = t.Node.Net.subscriptableOrigin('checker'); - -%% make initial layers to be used as templates -maskTemplate = vis.emptyLayer(); -maskTemplate.isPeriodic = false; -maskTemplate.interpolation = 'nearest'; -maskTemplate.show = true; -maskTemplate.colourMask = [false false false true]; - -maskTemplate.textureId = 'checkerMaskPixel'; -[maskTemplate.rgba, maskTemplate.rgbaSize] = vis.rgba(0, 0); -maskTemplate.blending = '1-source'; % allows us to lay down our zero alpha value - -stencilTemplate = maskTemplate; -stencilTemplate.textureId = 'checkerStencilPixel'; -[stencilTemplate.rgba, stencilTemplate.rgbaSize] = vis.rgba(1, 1); -stencilTemplate.blending = 'none'; - -% pattern layer uses the alpha values laid down by mask layers -patternLayer = vis.emptyLayer(); -patternLayer.textureId = sprintf('~checker%i', randi(2^32)); -patternLayer.isPeriodic = false; -patternLayer.interpolation = 'nearest'; -patternLayer.blending = 'destination'; % use the alpha mask gets laid down before this - -%% construct signals used to assemble layers -% N rows by cols signal is derived from the size of the pattern array but -% we skip repeats so that pattern changes don't update the mask layers -% unless the size has acutally changed -nRowsByCols = elem.pattern.flatten().map(@size).skipRepeats(); -aziRange = elem.azimuthRange.flatten(); -altRange = elem.altitudeRange.flatten(); -sizeFrac = elem.rectSizeFrac.flatten(); -% signal containing the masking layers -gridMaskLayers = mapn(nRowsByCols, aziRange, altRange, sizeFrac, ... - maskTemplate, stencilTemplate, @gridMask); -% signal contain the checker layer -checkerLayer = scan(elem.pattern.flatten(), @updatePattern,... - elem.colour.flatten(), @updateColour,... - elem.azimuthRange.flatten(), @updateAzi,... - elem.altitudeRange.flatten(), @updateAlt,... - elem.show.flatten(), @updateShow,... - patternLayer); % initial value -%% set default attribute values -elem.layers = [gridMaskLayers checkerLayer]; -elem.azimuthRange = [-135 135]; -elem.altitudeRange = [-37.5 37.5]; -elem.rectSizeFrac = [1 1]; % horizontal and vertical size of each rectangle -elem.pattern = [ - 1 -1 1 -1 - -1 0 0 0 - 1 0 0 0 - -1 1 -1 1]; - elem.show = true; -end - -%% helper functions -function layer = updatePattern(layer, pattern) -% map pattern from -1 -> 1 range to 0->255, cast to 8 bit integers, then -% convert to RGBA texture format. -[layer.rgba, layer.rgbaSize] = vis.rgbaFromUint8(uint8(127.5*(1 + pattern)), 1); -end - -function layer = updateColour(layer, colour) -layer.maxColour = [colour 1]; -end - -function layer = updateAzi(layer, aziRange) -layer.size(1) = abs(diff(aziRange)); -layer.texOffset(1) = mean(aziRange); -end - -function layer = updateAlt(layer, altRange) -layer.size(2) = abs(diff(altRange)); -layer.texOffset(2) = mean(altRange); -end - -function layer = updateShow(layer, show) -layer.show = show; -end - -function layers = gridMask(nRowsByCols, aziRange, altRange, sizeFrac, mask, stencil) -gridDims = [abs(diff(aziRange)) abs(diff(altRange))]; -cellSize = gridDims./flip(nRowsByCols); -nCols = nRowsByCols(2) + 1; -nRows = nRowsByCols(1) + 1; -midAzi = mean(aziRange); -midAlt = mean(altRange); -%% base layer to imprint area the checker can draw on (by applying an alpha mask) -stencil.texOffset = [midAzi midAlt]; -stencil.size = gridDims; -if any(sizeFrac < 1) - %% layers for lines making up mask grid - masks out margins around each square - % make layers for vertical lines - if nCols > 1 - azi = linspace(aziRange(1), aziRange(2), nCols); - else - azi = midAzi; - end - collayers = repmat(mask, 1, nCols); - for vi = 1:nCols - collayers(vi).texOffset = [azi(vi) midAlt]; - end - [collayers.size] = deal([(1 - sizeFrac(1))*cellSize(1) gridDims(2)]); - % make layers for horizontal lines - if nRows > 1 - alt = linspace(altRange(1), altRange(2), nRows); - else - alt = midAlt; - end - rowlayers = repmat(mask, 1, nRows); - for hi = 1:nRows - rowlayers(hi).texOffset = [midAzi alt(hi)]; - end - [rowlayers.size] = deal([gridDims(1) (1 - sizeFrac(2))*cellSize(2)]); - %% combine the layers and return - layers = [stencil collayers rowlayers]; -else % no mask grid needed as each cell is full size - layers = stencil; -end - -end \ No newline at end of file diff --git a/cortexlab/+vis/checkerLeft.m b/cortexlab/+vis/checkerLeft.m deleted file mode 100644 index cf1122d7..00000000 --- a/cortexlab/+vis/checkerLeft.m +++ /dev/null @@ -1,126 +0,0 @@ -function elem = checkerLeft(t) -%vis.checker A grid of rectangles -% Detailed explanation goes here - -elem = t.Node.Net.subscriptableOrigin('checker'); - -%% make initial layers to be used as templates -maskTemplate = vis.emptyLayer(); -maskTemplate.isPeriodic = false; -maskTemplate.interpolation = 'nearest'; -maskTemplate.show = true; -maskTemplate.colourMask = [false false false true]; - -maskTemplate.textureId = 'checkerMaskPixel'; -[maskTemplate.rgba, maskTemplate.rgbaSize] = vis.rgba(0, 0); -maskTemplate.blending = '1-source'; % allows us to lay down our zero alpha value - -stencilTemplate = maskTemplate; -stencilTemplate.textureId = 'checkerStencilPixel'; -[stencilTemplate.rgba, stencilTemplate.rgbaSize] = vis.rgba(1, 1); -stencilTemplate.blending = 'none'; - -% pattern layer uses the alpha values laid down by mask layers -patternLayer = vis.emptyLayer(); -patternLayer.textureId = sprintf('~checker%i', randi(2^32)); -patternLayer.isPeriodic = false; -patternLayer.interpolation = 'nearest'; -patternLayer.blending = 'destination'; % use the alpha mask gets laid down before this - -%% construct signals used to assemble layers -% N rows by cols signal is derived from the size of the pattern array but -% we skip repeats so that pattern changes don't update the mask layers -% unless the size has acutally changed -nRowsByCols = elem.pattern.flatten().map(@size).skipRepeats(); -aziRange = elem.azimuthRange.flatten(); -altRange = elem.altitudeRange.flatten(); -sizeFrac = elem.rectSizeFrac.flatten(); -% signal containing the masking layers -gridMaskLayers = mapn(nRowsByCols, aziRange, altRange, sizeFrac, ... - maskTemplate, stencilTemplate, @gridMask); -% signal contain the checker layer -checkerLayer = scan(elem.pattern.flatten(), @updatePattern,... - elem.colour.flatten(), @updateColour,... - elem.azimuthRange.flatten(), @updateAzi,... - elem.altitudeRange.flatten(), @updateAlt,... - elem.show.flatten(), @updateShow,... - patternLayer); % initial value -%% set default attribute values -elem.layers = [gridMaskLayers checkerLayer]; -elem.azimuthRange = [-135 0]; -elem.altitudeRange = [-37.5 37.5]; -elem.rectSizeFrac = [1 1]; % horizontal and vertical size of each rectangle -elem.pattern = [ - 1 -1 1 -1 - -1 0 0 0 - 1 0 0 0 - -1 1 -1 1]; - elem.show = true; -end - -%% helper functions -function layer = updatePattern(layer, pattern) -% map pattern from -1 -> 1 range to 0->255, cast to 8 bit integers, then -% convert to RGBA texture format. -[layer.rgba, layer.rgbaSize] = vis.rgbaFromUint8(uint8(127.5*(1 + pattern)), 1); -end - -function layer = updateColour(layer, colour) -layer.maxColour = [colour 1]; -end - -function layer = updateAzi(layer, aziRange) -layer.size(1) = abs(diff(aziRange)); -layer.texOffset(1) = mean(aziRange); -end - -function layer = updateAlt(layer, altRange) -layer.size(2) = abs(diff(altRange)); -layer.texOffset(2) = mean(altRange); -end - -function layer = updateShow(layer, show) -layer.show = show; -end - -function layers = gridMask(nRowsByCols, aziRange, altRange, sizeFrac, mask, stencil) -gridDims = [abs(diff(aziRange)) abs(diff(altRange))]; -cellSize = gridDims./flip(nRowsByCols); -nCols = nRowsByCols(2) + 1; -nRows = nRowsByCols(1) + 1; -midAzi = mean(aziRange); -midAlt = mean(altRange); -%% base layer to imprint area the checker can draw on (by applying an alpha mask) -stencil.texOffset = [midAzi midAlt]; -stencil.size = gridDims; -if any(sizeFrac < 1) - %% layers for lines making up mask grid - masks out margins around each square - % make layers for vertical lines - if nCols > 1 - azi = linspace(aziRange(1), aziRange(2), nCols); - else - azi = midAzi; - end - collayers = repmat(mask, 1, nCols); - for vi = 1:nCols - collayers(vi).texOffset = [azi(vi) midAlt]; - end - [collayers.size] = deal([(1 - sizeFrac(1))*cellSize(1) gridDims(2)]); - % make layers for horizontal lines - if nRows > 1 - alt = linspace(altRange(1), altRange(2), nRows); - else - alt = midAlt; - end - rowlayers = repmat(mask, 1, nRows); - for hi = 1:nRows - rowlayers(hi).texOffset = [midAzi alt(hi)]; - end - [rowlayers.size] = deal([gridDims(1) (1 - sizeFrac(2))*cellSize(2)]); - %% combine the layers and return - layers = [stencil collayers rowlayers]; -else % no mask grid needed as each cell is full size - layers = stencil; -end - -end \ No newline at end of file diff --git a/cortexlab/+vis/checkerRight.m b/cortexlab/+vis/checkerRight.m deleted file mode 100644 index 78b03545..00000000 --- a/cortexlab/+vis/checkerRight.m +++ /dev/null @@ -1,126 +0,0 @@ -function elem = checkerRight(t) -%vis.checker A grid of rectangles -% Detailed explanation goes here - -elem = t.Node.Net.subscriptableOrigin('checker'); - -%% make initial layers to be used as templates -maskTemplate = vis.emptyLayer(); -maskTemplate.isPeriodic = false; -maskTemplate.interpolation = 'nearest'; -maskTemplate.show = true; -maskTemplate.colourMask = [false false false true]; - -maskTemplate.textureId = 'checkerMaskPixel'; -[maskTemplate.rgba, maskTemplate.rgbaSize] = vis.rgba(0, 0); -maskTemplate.blending = '1-source'; % allows us to lay down our zero alpha value - -stencilTemplate = maskTemplate; -stencilTemplate.textureId = 'checkerStencilPixel'; -[stencilTemplate.rgba, stencilTemplate.rgbaSize] = vis.rgba(1, 1); -stencilTemplate.blending = 'none'; - -% pattern layer uses the alpha values laid down by mask layers -patternLayer = vis.emptyLayer(); -patternLayer.textureId = sprintf('~checker%i', randi(2^32)); -patternLayer.isPeriodic = false; -patternLayer.interpolation = 'nearest'; -patternLayer.blending = 'destination'; % use the alpha mask gets laid down before this - -%% construct signals used to assemble layers -% N rows by cols signal is derived from the size of the pattern array but -% we skip repeats so that pattern changes don't update the mask layers -% unless the size has acutally changed -nRowsByCols = elem.pattern.flatten().map(@size).skipRepeats(); -aziRange = elem.azimuthRange.flatten(); -altRange = elem.altitudeRange.flatten(); -sizeFrac = elem.rectSizeFrac.flatten(); -% signal containing the masking layers -gridMaskLayers = mapn(nRowsByCols, aziRange, altRange, sizeFrac, ... - maskTemplate, stencilTemplate, @gridMask); -% signal contain the checker layer -checkerLayer = scan(elem.pattern.flatten(), @updatePattern,... - elem.colour.flatten(), @updateColour,... - elem.azimuthRange.flatten(), @updateAzi,... - elem.altitudeRange.flatten(), @updateAlt,... - elem.show.flatten(), @updateShow,... - patternLayer); % initial value -%% set default attribute values -elem.layers = [gridMaskLayers checkerLayer]; -elem.azimuthRange = [0 135]; -elem.altitudeRange = [-37.5 37.5]; -elem.rectSizeFrac = [1 1]; % horizontal and vertical size of each rectangle -elem.pattern = [ - 1 -1 1 -1 - -1 0 0 0 - 1 0 0 0 - -1 1 -1 1]; - elem.show = true; -end - -%% helper functions -function layer = updatePattern(layer, pattern) -% map pattern from -1 -> 1 range to 0->255, cast to 8 bit integers, then -% convert to RGBA texture format. -[layer.rgba, layer.rgbaSize] = vis.rgbaFromUint8(uint8(127.5*(1 + pattern)), 1); -end - -function layer = updateColour(layer, colour) -layer.maxColour = [colour 1]; -end - -function layer = updateAzi(layer, aziRange) -layer.size(1) = abs(diff(aziRange)); -layer.texOffset(1) = mean(aziRange); -end - -function layer = updateAlt(layer, altRange) -layer.size(2) = abs(diff(altRange)); -layer.texOffset(2) = mean(altRange); -end - -function layer = updateShow(layer, show) -layer.show = show; -end - -function layers = gridMask(nRowsByCols, aziRange, altRange, sizeFrac, mask, stencil) -gridDims = [abs(diff(aziRange)) abs(diff(altRange))]; -cellSize = gridDims./flip(nRowsByCols); -nCols = nRowsByCols(2) + 1; -nRows = nRowsByCols(1) + 1; -midAzi = mean(aziRange); -midAlt = mean(altRange); -%% base layer to imprint area the checker can draw on (by applying an alpha mask) -stencil.texOffset = [midAzi midAlt]; -stencil.size = gridDims; -if any(sizeFrac < 1) - %% layers for lines making up mask grid - masks out margins around each square - % make layers for vertical lines - if nCols > 1 - azi = linspace(aziRange(1), aziRange(2), nCols); - else - azi = midAzi; - end - collayers = repmat(mask, 1, nCols); - for vi = 1:nCols - collayers(vi).texOffset = [azi(vi) midAlt]; - end - [collayers.size] = deal([(1 - sizeFrac(1))*cellSize(1) gridDims(2)]); - % make layers for horizontal lines - if nRows > 1 - alt = linspace(altRange(1), altRange(2), nRows); - else - alt = midAlt; - end - rowlayers = repmat(mask, 1, nRows); - for hi = 1:nRows - rowlayers(hi).texOffset = [midAzi alt(hi)]; - end - [rowlayers.size] = deal([gridDims(1) (1 - sizeFrac(2))*cellSize(2)]); - %% combine the layers and return - layers = [stencil collayers rowlayers]; -else % no mask grid needed as each cell is full size - layers = stencil; -end - -end \ No newline at end of file diff --git a/signals b/signals index 897da00e..924de6a5 160000 --- a/signals +++ b/signals @@ -1 +1 @@ -Subproject commit 897da00ee217874db2f271b9dbad42886eb4ff7d +Subproject commit 924de6a5b850e1f797a5ad6b24e64644fb5b1f00 From 770499b4e9479c0cedb058bff2142f8c3b86e4d0 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Wed, 23 Jan 2019 01:34:36 +0200 Subject: [PATCH 002/108] cellFlat now works with arrays of Signals objects --- cb-tools/burgbox/cellflat.m | 2 +- signals | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cb-tools/burgbox/cellflat.m b/cb-tools/burgbox/cellflat.m index 532b011f..37a5252d 100644 --- a/cb-tools/burgbox/cellflat.m +++ b/cb-tools/burgbox/cellflat.m @@ -18,7 +18,7 @@ if isempty(elem) elem = {elem}; end - flat = [flat; elem]; + flat = [flat; ensureCell(elem)]; end end diff --git a/signals b/signals index 924de6a5..ebea3f63 160000 --- a/signals +++ b/signals @@ -1 +1 @@ -Subproject commit 924de6a5b850e1f797a5ad6b24e64644fb5b1f00 +Subproject commit ebea3f63a892edf25fc8b4d81156a219283c4a4c From 72acc9009c325197e5e20b6247488d24d7bca20f Mon Sep 17 00:00:00 2001 From: k1o0 Date: Thu, 24 Jan 2019 18:30:19 +0200 Subject: [PATCH 003/108] Comments still saved loaclly upon failure to post to Alyx --- +dat/updateLogEntry.m | 6 +++++- signals | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/+dat/updateLogEntry.m b/+dat/updateLogEntry.m index 0882ba2a..47678185 100644 --- a/+dat/updateLogEntry.m +++ b/+dat/updateLogEntry.m @@ -13,7 +13,11 @@ function updateLogEntry(subject, id, newEntry) if isfield(newEntry, 'AlyxInstance') % Update session narrative on Alyx if ~isempty(newEntry.comments) && ~strcmp(subject, 'default') - newEntry.comments = newEntry.AlyxInstance.updateNarrative(newEntry.comments); + try + newEntry.comments = newEntry.AlyxInstance.updateNarrative(newEntry.comments); + catch + warning('Alyx:updateNarrative:UploadFailed', 'Failed to update Alyx session narrative'); + end end newEntry = rmfield(newEntry, 'AlyxInstance'); end diff --git a/signals b/signals index ebea3f63..e05d9537 160000 --- a/signals +++ b/signals @@ -1 +1 @@ -Subproject commit ebea3f63a892edf25fc8b4d81156a219283c4a4c +Subproject commit e05d95376fdc1d36b2dac8e55ed9484c3e6edded From 9ad2c06d3d6a02f7624c23fa781dc83b158024b2 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Thu, 24 Jan 2019 18:48:15 +0200 Subject: [PATCH 004/108] Updates to session done with PATCH --- +exp/SignalsExp.m | 4 ++-- alyx-matlab | 2 +- cortexlab/+exp/ChoiceWorld.m | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/+exp/SignalsExp.m b/+exp/SignalsExp.m index 2eb9d192..f616ea70 100644 --- a/+exp/SignalsExp.m +++ b/+exp/SignalsExp.m @@ -906,10 +906,10 @@ function saveData(obj) numCorrect = 0; end % Update Alyx session with end time, trial counts and water tye - sessionData = struct('end_time', obj.AlyxInstance.datestr(now), 'subject', subject); + sessionData = struct('end_time', obj.AlyxInstance.datestr(now)); if ~isempty(numTrials); sessionData.n_trials = numTrials; end if ~isempty(numCorrect); sessionData.n_correct_trials = numCorrect; end - obj.AlyxInstance.postData(url, sessionData, 'put'); + obj.AlyxInstance.postData(url, sessionData, 'patch'); else % Retrieve session from endpoint % subsessions = obj.AlyxInstance.getData(... diff --git a/alyx-matlab b/alyx-matlab index 6fc933b9..dd2ab36d 160000 --- a/alyx-matlab +++ b/alyx-matlab @@ -1 +1 @@ -Subproject commit 6fc933b99bec09689a83b024284e8023d2c5793d +Subproject commit dd2ab36de59843edc94c15956967731d81173b74 diff --git a/cortexlab/+exp/ChoiceWorld.m b/cortexlab/+exp/ChoiceWorld.m index 607d6d06..60ad59ec 100644 --- a/cortexlab/+exp/ChoiceWorld.m +++ b/cortexlab/+exp/ChoiceWorld.m @@ -194,8 +194,8 @@ function saveData(obj) numCorrect = 0; end sessionData = struct('end_time', obj.AlyxInstance.datestr(now), ... - 'subject', subject, 'n_trials', numTrials, 'n_correct_trials', numCorrect); - obj.AlyxInstance.postData(obj.AlyxInstance.SessionURL, sessionData, 'put'); + 'n_trials', numTrials, 'n_correct_trials', numCorrect); + obj.AlyxInstance.postData(obj.AlyxInstance.SessionURL, sessionData, 'patch'); else % Infer from date session and retrieve using expFilePath end From d54582b72d6a57957a7a2719e2df5b6fbb6bc2ae Mon Sep 17 00:00:00 2001 From: k1o0 Date: Thu, 24 Jan 2019 19:18:28 +0200 Subject: [PATCH 005/108] Added change to patch cached put files to Alyx --- cortexlab/+git/changes.m | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 cortexlab/+git/changes.m diff --git a/cortexlab/+git/changes.m b/cortexlab/+git/changes.m new file mode 100644 index 00000000..d9e9f013 --- /dev/null +++ b/cortexlab/+git/changes.m @@ -0,0 +1,6 @@ +disp('Updating queued Alyx posts...') +posts = dirPlus(getOr(dat.paths, 'localAlyxQueue', 'C:/localAlyxQueue')); +posts = posts(endsWith(posts, 'put')); +newPosts = cellfun(@(str)[str(1:end-3) 'patch'], posts, 'uni', 0); +status = cellfun(@movefile, posts, newPosts); +assert(all(status), 'Unable to rename queued Alyx files, please do this manually') \ No newline at end of file From 7ad3bab93386a26c83cbd9b4757eedbf09b96a8c Mon Sep 17 00:00:00 2001 From: k1o0 Date: Sat, 26 Jan 2019 16:45:25 +0200 Subject: [PATCH 006/108] Fix bug for when code never fetched --- cortexlab/+git/update.m | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cortexlab/+git/update.m b/cortexlab/+git/update.m index 86cb25e9..aaf67546 100644 --- a/cortexlab/+git/update.m +++ b/cortexlab/+git/update.m @@ -9,7 +9,12 @@ function update(scheduled) if nargin < 1; scheduled = getOr(dat.paths, 'updateSchedule', 0); end root = fileparts(which('addRigboxPaths')); -lastFetch = getOr(dir(fullfile(root, '.git', 'FETCH_HEAD')), 'datenum'); +% Attempt to find date of last fetch +fetch_head = fullfile(root, '.git', 'FETCH_HEAD'); +lastFetch = iff(exist(fetch_head,'file')==2, ... % If FETCH_HEAD file exists + @()getOr(dir(fetch_head), 'datenum'), 0); % Retrieve date modified +% If the code has not been fetched in over a week, force and update, +% otherwise return if (scheduled && weekday(now) ~= scheduled && now - lastFetch < 7) || ... (~scheduled && now - lastFetch < 1/24) return From 7ae881309bc8413506b13e1449cf546f97f7266c Mon Sep 17 00:00:00 2001 From: k1o0 Date: Sun, 27 Jan 2019 17:03:20 +0200 Subject: [PATCH 007/108] Bug fix for timeplot --- signals | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/signals b/signals index e05d9537..93520307 160000 --- a/signals +++ b/signals @@ -1 +1 @@ -Subproject commit e05d95376fdc1d36b2dac8e55ed9484c3e6edded +Subproject commit 93520307c1cb1a9dc12fa8bda685a01b9ab80401 From 5dcb8285193ccce8c7b816d3848511402ef89c46 Mon Sep 17 00:00:00 2001 From: jaib1 Date: Mon, 28 Jan 2019 08:16:59 +0000 Subject: [PATCH 008/108] add signals 'tutorials' folder to paths --- addRigboxPaths.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addRigboxPaths.m b/addRigboxPaths.m index b705367e..0364b06f 100644 --- a/addRigboxPaths.m +++ b/addRigboxPaths.m @@ -94,8 +94,8 @@ function addRigboxPaths(savePaths) % Add signals paths, this includes all the core code for running signals % experiments. This submodule is maintained by Chris Burgess. addpath(fullfile(root, 'signals'),... - fullfile(root, 'signals', 'mexnet'),... - fullfile(root, 'signals', 'util')); + fullfile(root, 'signals', 'mexnet'), fullfile(root, 'signals', 'util'),... + fullfile(root, 'signals', 'tutorials')); % Add the Java paths for signals jcp = fullfile(root, 'signals', 'java'); if ~any(strcmp(javaclasspath, jcp)); javaaddpath(jcp); end From 808d565bab222827b60f410842868d09c873b960 Mon Sep 17 00:00:00 2001 From: jaib1 Date: Mon, 28 Jan 2019 12:27:38 +0000 Subject: [PATCH 009/108] Revert "add signals 'tutorials' folder to paths" This reverts commit 5dcb8285193ccce8c7b816d3848511402ef89c46. --- addRigboxPaths.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addRigboxPaths.m b/addRigboxPaths.m index 0364b06f..b705367e 100644 --- a/addRigboxPaths.m +++ b/addRigboxPaths.m @@ -94,8 +94,8 @@ function addRigboxPaths(savePaths) % Add signals paths, this includes all the core code for running signals % experiments. This submodule is maintained by Chris Burgess. addpath(fullfile(root, 'signals'),... - fullfile(root, 'signals', 'mexnet'), fullfile(root, 'signals', 'util'),... - fullfile(root, 'signals', 'tutorials')); + fullfile(root, 'signals', 'mexnet'),... + fullfile(root, 'signals', 'util')); % Add the Java paths for signals jcp = fullfile(root, 'signals', 'java'); if ~any(strcmp(javaclasspath, jcp)); javaaddpath(jcp); end From 3d90407b2353337a7e5d790537a0fc791a9abbf5 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Mon, 28 Jan 2019 17:43:37 +0200 Subject: [PATCH 010/108] Update only occurs once on scheduled day --- cortexlab/+git/update.m | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/cortexlab/+git/update.m b/cortexlab/+git/update.m index aaf67546..fd6a7e0b 100644 --- a/cortexlab/+git/update.m +++ b/cortexlab/+git/update.m @@ -1,11 +1,15 @@ function update(scheduled) % GIT.UPDATE Pull latest Rigbox code % Pulls the latest code from the remote repository. If scheduled is a -% value in the range [1 7] corresponding to the days of the week, the -% function will only continue on that day, or if the last fetch was over -% a week ago. +% value in the range [1 7] corresponding to the days of the week starting +% Sunday, the function will only continue on that day, provided the last +% fetch was over a day ago. If it is not the scheduled day, but the last +% fetch was over a week ago, the function will pull changes. If +% scheduled is false, the function will pull changes provided the last +% fetch was over an hour ago. +% % TODO Find quicker way to check for changes -% See also +% See also DAT.PATHS if nargin < 1; scheduled = getOr(dat.paths, 'updateSchedule', 0); end root = fileparts(which('addRigboxPaths')); @@ -13,9 +17,15 @@ function update(scheduled) fetch_head = fullfile(root, '.git', 'FETCH_HEAD'); lastFetch = iff(exist(fetch_head,'file')==2, ... % If FETCH_HEAD file exists @()getOr(dir(fetch_head), 'datenum'), 0); % Retrieve date modified -% If the code has not been fetched in over a week, force and update, -% otherwise return + +% Don't pull changes if the following conditions are met: +% 1. The updates are scheduled for a different day and the last fetch was less +% than a week ago. +% 2. The updates are scheduled for today and the last fetch was today. +% 3. The updates are scheduled for every day and the last fetch was less +% than an hour ago. if (scheduled && weekday(now) ~= scheduled && now - lastFetch < 7) || ... + (scheduled && weekday(now) == scheduled && now - lastFetch < 1) || ... (~scheduled && now - lastFetch < 1/24) return end @@ -39,10 +49,10 @@ function update(scheduled) % Stash any WIP, check submodules are initialized, pull try - [status, cmdout] = system(cmdstrStash, '-echo'); - [status, cmdout] = system(cmdstrStashSubs, '-echo'); - [status, cmdout] = system(cmdstrInit, '-echo'); - [status, cmdout] = system(cmdstrPull, '-echo'); + [~, cmdout] = system(cmdstrStash, '-echo'); + [~, cmdout] = system(cmdstrStashSubs, '-echo'); + [~, cmdout] = system(cmdstrInit, '-echo'); + [~, cmdout] = system(cmdstrPull, '-echo'); %#ok catch ex cd(origDir) error('gitUpdate:pull:pullFailed', 'Failed to pull latest changes:, %s', cmdout) @@ -55,4 +65,4 @@ function update(scheduled) delete(changesPath); end cd(origDir) -end +end \ No newline at end of file From 0ff74273ede9adfe8ec372ae13d9a41997cad8a7 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Mon, 28 Jan 2019 16:22:31 +0000 Subject: [PATCH 011/108] additional comments for git.update --- cortexlab/+git/update.m | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/cortexlab/+git/update.m b/cortexlab/+git/update.m index fd6a7e0b..a04a9b62 100644 --- a/cortexlab/+git/update.m +++ b/cortexlab/+git/update.m @@ -1,32 +1,34 @@ function update(scheduled) % GIT.UPDATE Pull latest Rigbox code -% Pulls the latest code from the remote repository. If scheduled is a -% value in the range [1 7] corresponding to the days of the week starting -% Sunday, the function will only continue on that day, provided the last -% fetch was over a day ago. If it is not the scheduled day, but the last -% fetch was over a week ago, the function will pull changes. If -% scheduled is false, the function will pull changes provided the last -% fetch was over an hour ago. +% Pulls the latest code from the remote Github repository. If 'scheduled' +% is a value in the range [1 7] - corresponding to the days of the week, +% with Sunday=1 - code will be pulled only on the 'scheduled' day, +% provided the last fetch was over a day ago. Code will also be pulled if +% it is not the scheduled day, but the last fetch was over a week ago. If +% scheduled is 0, the function will pull changes provided the last fetch +% was over an hour ago. % % TODO Find quicker way to check for changes % See also DAT.PATHS -if nargin < 1; scheduled = getOr(dat.paths, 'updateSchedule', 0); end -root = fileparts(which('addRigboxPaths')); +% If not given as input argument, find 'scheduled' in 'dat.paths'. If not +% found, set 'scheduled' to 0. +if nargin < 1; scheduled = getOr(dat.paths, 'updateSchedule', 0); end +root = fileparts(which('addRigboxPaths')); % Rigbox root directory % Attempt to find date of last fetch fetch_head = fullfile(root, '.git', 'FETCH_HEAD'); lastFetch = iff(exist(fetch_head,'file')==2, ... % If FETCH_HEAD file exists @()getOr(dir(fetch_head), 'datenum'), 0); % Retrieve date modified % Don't pull changes if the following conditions are met: -% 1. The updates are scheduled for a different day and the last fetch was less -% than a week ago. +% 1. The updates are scheduled for a different day and the last fetch was +% less than a week ago. % 2. The updates are scheduled for today and the last fetch was today. % 3. The updates are scheduled for every day and the last fetch was less % than an hour ago. -if (scheduled && weekday(now) ~= scheduled && now - lastFetch < 7) || ... - (scheduled && weekday(now) == scheduled && now - lastFetch < 1) || ... - (~scheduled && now - lastFetch < 1/24) +if ((scheduled && weekday(now)) ~= (scheduled && (now - lastFetch < 7))) || ... + ((scheduled && weekday(now)) == (scheduled && (now - lastFetch < 1))) || ... + (~scheduled && (now - lastFetch < 1/24)) return end disp('Updating code...') @@ -34,7 +36,7 @@ function update(scheduled) % Get the path to the Git exe gitexepath = getOr(dat.paths, 'gitExe'); if isempty(gitexepath) - [~,gitexepath] = system('where git'); % this doesn't always work + [~,gitexepath] = system('where git'); % todo: this doesn't always work end gitexepath = ['"', strtrim(gitexepath), '"']; @@ -42,6 +44,8 @@ function update(scheduled) origDir = pwd; cd(root) +% Create Windows system commands for git stashing, initializing submodules, +% and pulling cmdstrStash = [gitexepath, ' stash push -m "stash Rigbox working changes before scheduled git update"']; cmdstrStashSubs = [gitexepath, ' submodule foreach "git stash push"']; cmdstrInit = [gitexepath, ' submodule update --init']; @@ -65,4 +69,4 @@ function update(scheduled) delete(changesPath); end cd(origDir) -end \ No newline at end of file +end From c9c27a22749d09111754585a90a2528d261598f3 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Mon, 28 Jan 2019 19:53:47 +0200 Subject: [PATCH 012/108] Fix'd incorrect brackets --- cortexlab/+git/update.m | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cortexlab/+git/update.m b/cortexlab/+git/update.m index a04a9b62..200ba269 100644 --- a/cortexlab/+git/update.m +++ b/cortexlab/+git/update.m @@ -11,8 +11,8 @@ function update(scheduled) % TODO Find quicker way to check for changes % See also DAT.PATHS -% If not given as input argument, find 'scheduled' in 'dat.paths'. If not -% found, set 'scheduled' to 0. +% If not given as input argument, use 'updateSchedule' in 'dat.paths'. If +% not found, set 'scheduled' to 0. if nargin < 1; scheduled = getOr(dat.paths, 'updateSchedule', 0); end root = fileparts(which('addRigboxPaths')); % Rigbox root directory % Attempt to find date of last fetch @@ -26,9 +26,9 @@ function update(scheduled) % 2. The updates are scheduled for today and the last fetch was today. % 3. The updates are scheduled for every day and the last fetch was less % than an hour ago. -if ((scheduled && weekday(now)) ~= (scheduled && (now - lastFetch < 7))) || ... - ((scheduled && weekday(now)) == (scheduled && (now - lastFetch < 1))) || ... - (~scheduled && (now - lastFetch < 1/24)) +if (scheduled && (weekday(now) ~= scheduled) && now - lastFetch < 7) || ... + (scheduled && (weekday(now) == scheduled) && now - lastFetch < 1) || ... + (~scheduled && now - lastFetch < 1/24) return end disp('Updating code...') From b36cadb46f9b9deec89de5d6edf93aec00237bb8 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Thu, 31 Jan 2019 00:38:11 +0200 Subject: [PATCH 013/108] Attempt to integrate new Parameter Editor into mc --- +eui/ConditionPanel.m | 252 ++++++++++++++++ +eui/FieldPanel.m | 164 +++++++++++ +eui/MControl.m | 24 +- +eui/ParamEditor.m | 630 +++++++++++++--------------------------- +eui/ParamEditor_old.m | 516 ++++++++++++++++++++++++++++++++ +exp/Parameters.m | 10 +- cortexlab/+git/update.m | 2 +- signals | 2 +- 8 files changed, 1160 insertions(+), 440 deletions(-) create mode 100644 +eui/ConditionPanel.m create mode 100644 +eui/FieldPanel.m create mode 100644 +eui/ParamEditor_old.m diff --git a/+eui/ConditionPanel.m b/+eui/ConditionPanel.m new file mode 100644 index 00000000..7c4b89fe --- /dev/null +++ b/+eui/ConditionPanel.m @@ -0,0 +1,252 @@ +classdef ConditionPanel < handle + %UNTITLED Summary of this class goes here + % Detailed explanation goes here + % TODO Document + % TODO Add sort by column + % TODO Add set condition idx + % TODO Use tags for menu items + + properties + ConditionTable + MinWidth = 80 +% MaxWidth = 140 +% Margin = 4 + UIPanel + ButtonPanel + ContextMenus + end + + properties %(Access = protected) + ParamEditor + Listener + NewConditionButton + DeleteConditionButton + MakeGlobalButton + SetValuesButton + SelectedCells %[row, column;...] of each selected cell + end + + methods + function obj = ConditionPanel(f, ParamEditor, varargin) + obj.ParamEditor = ParamEditor; + obj.UIPanel = uipanel('Parent', f, 'BorderType', 'none',... + 'BackgroundColor', 'white', 'Position', [0.5 0.05 0.5 0.95]); + % Create a child menu for the uiContextMenus + c = uicontextmenu; + obj.UIPanel.UIContextMenu = c; + obj.ContextMenus = uimenu(c, 'Label', 'Make Global', 'MenuSelectedFcn', @(~,~)obj.makeGlobal); + fcn = @(s,~)obj.ParamEditor.setRandomized(~strcmp(s.Checked, 'on')); + obj.ContextMenus(2) = uimenu(c, 'Label', 'Randomize conditions', ... + 'MenuSelectedFcn', fcn, 'Checked', 'on', 'Tag', 'randomize button'); + obj.ContextMenus(3) = uimenu(c, 'Label', 'Sort by selected column', ... + 'MenuSelectedFcn', @(~,~)disp('feature not yet implemented'), 'Tag', 'sort by'); + % Create condition table + obj.ConditionTable = uitable('Parent', obj.UIPanel,... + 'FontName', 'Consolas',... + 'RowName', [],... + 'RearrangeableColumns', true,... + 'Units', 'normalized',... + 'Position',[0 0 1 1],... + 'UIContextMenu', c,... + 'CellEditCallback', @obj.onEdit,... + 'CellSelectionCallback', @obj.onSelect); + % Create button panel to hold condition control buttons + obj.ButtonPanel = uipanel('BackgroundColor', 'white',... + 'Position', [0.5 0 0.5 0.05], 'BorderType', 'none'); + % Create callback so that width of button panel is slave to width of + % conditional UIPanel + b = obj.ButtonPanel; + fcn = @(s)set(obj.ButtonPanel, 'Position', ... + [s.Position(1) b.Position(2) s.Position(3) b.Position(4)]); + obj.Listener = event.listener(obj.UIPanel, 'SizeChanged', @(s,~)fcn(s)); + % Define some common properties + props.BackgroundColor = 'white'; + props.Style = 'pushbutton'; + props.Units = 'normalized'; + props.Parent = obj.ButtonPanel; + % Create out four buttons + obj.NewConditionButton = uicontrol(props,... + 'String', 'New condition',... + 'Position',[0 0 1/4 1],... + 'TooltipString', 'Add a new condition',... + 'Callback', @(~, ~) obj.newCondition()); + obj.DeleteConditionButton = uicontrol(props,... + 'String', 'Delete condition',... + 'Position',[1/4 0 1/4 1],... + 'TooltipString', 'Delete the selected condition',... + 'Enable', 'off',... + 'Callback', @(~, ~) obj.deleteSelectedConditions()); + obj.MakeGlobalButton = uicontrol(props,... + 'String', 'Globalise parameter',... + 'Position',[2/4 0 1/4 1],... + 'TooltipString', sprintf(['Make the selected condition-specific parameter global (i.e. not vary by trial)\n'... + 'This will move it to the global parameters section']),... + 'Enable', 'off',... + 'Callback', @(~, ~) obj.makeGlobal()); + obj.SetValuesButton = uicontrol(props,... + 'String', 'Set values',... + 'Position',[3/4 0 1/4 1],... + 'TooltipString', 'Set selected values to specified value, range or function',... + 'Enable', 'off',... + 'Callback', @(~, ~) obj.setSelectedValues()); + end + + function onEdit(obj, src, eventData) + disp('updating table cell'); + row = eventData.Indices(1); + col = eventData.Indices(2); + paramName = obj.ConditionTable.ColumnName{col}; + newValue = obj.ParamEditor.update(paramName, eventData.NewData, row); + reformed = obj.ParamEditor.paramValue2Control(newValue); + % If successful update the cell with default formatting + data = get(src, 'Data'); + if iscell(reformed) + % The reformed data type is a cell, this should be a one element + % wrapping cell + if numel(reformed) == 1 + reformed = reformed{1}; + else + error('Cannot handle data reformatted data type'); + end + end + data{row,col} = reformed; + set(src, 'Data', data); + end + + function clear(obj) + set(obj.ConditionTable, 'ColumnName', [], ... + 'Data', [], 'ColumnEditable', false); + end + + function delete(obj) + disp('delete called'); + delete(obj.UIPanel); + end + + function onSelect(obj, ~, eventData) + obj.SelectedCells = eventData.Indices; + if size(eventData.Indices, 1) > 0 + % cells selected, enable buttons + set(obj.MakeGlobalButton, 'Enable', 'on'); + set(obj.DeleteConditionButton, 'Enable', 'on'); + set(obj.SetValuesButton, 'Enable', 'on'); + set(obj.ContextMenus(1), 'Enable', 'on'); + set(obj.ContextMenus(3), 'Enable', 'on'); + else + % nothing selected, disable buttons + set(obj.MakeGlobalButton, 'Enable', 'off'); + set(obj.DeleteConditionButton, 'Enable', 'off'); + set(obj.SetValuesButton, 'Enable', 'off'); + set(obj.ContextMenus(1), 'Enable', 'off'); + set(obj.ContextMenus(3), 'Enable', 'off'); + end + end + + function makeGlobal(obj) + if isempty(obj.SelectedCells) + disp('nothing selected') + return + end + [cols, iu] = unique(obj.SelectedCells(:,2)); + names = obj.ConditionTable.ColumnName(cols); + rows = num2cell(obj.SelectedCells(iu,1)); %get rows of unique selected cols + PE = obj.ParamEditor; + cellfun(@PE.globaliseParamAtCell, names, rows); + end + + function deleteSelectedConditions(obj) + %DELETESELECTEDCONDITIONS Removes the selected conditions from table + % The callback for the 'Delete condition' button. This removes the + % selected conditions from the table and if less than two conditions + % remain, globalizes them. + % TODO: comment function better, index in a clearer fashion + % + % See also EXP.PARAMETERS, GLOBALISESELECTEDPARAMETERS + rows = unique(obj.SelectedCells(:,1)); + names = obj.ConditionTable.ColumnName; + numConditions = size(obj.ConditionTable.Data,2); + % If the number of remaining conditions is 1 or less... + if numConditions-length(rows) <= 1 + remainingIdx = find(all(1:numConditions~=rows,1)); + if isempty(remainingIdx); remainingIdx = 1; end + % change selected cells to be all fields (except numRepeats which + % is assumed to always be the last column) + obj.SelectedCells =[ones(length(names),1)*remainingIdx, (1:length(names))']; + %... globalize them + obj.makeGlobal; + else % Otherwise delete the selected conditions as usual + obj.ParamEditor.Parameters.removeConditions(rows); %FIXME: Should be in ParamEditor + end + % Refresh the table of conditions FIXME: Should be in ParamEditor + obj.ParamEditor.fillConditionTable(); + end + + function setSelectedValues(obj) % Set multiple fields in conditional table + disp('updating table cells'); + cols = obj.SelectedCells(:,2); % selected columns + uCol = unique(obj.SelectedCells(:,2)); + rows = obj.SelectedCells(:,1); % selected rows + % get current values of selected cells + currVals = arrayfun(@(u)obj.ConditionTable.Data(rows(cols==u),u), uCol, 'UniformOutput', 0); + names = obj.ConditionTable.ColumnName(uCol); % selected column names + promt = cellfun(@(a,b) [a ' (' num2str(sum(cols==b)) ')'],... + names, num2cell(uCol), 'UniformOutput', 0); % names of columns & num selected rows + defaultans = cellfun(@(c) c(1), currVals); + answer = inputdlg(promt,'Set values', 1, cellflat(defaultans)); % prompt for input + if isempty(answer) % if user presses cancel + return + end + % set values for each column + cellfun(@(a,b,c) setNewVals(a,b,c), answer, currVals, names, 'UniformOutput', 0); + function newVals = setNewVals(userIn, currVals, paramName) + % check array orientation + currVals = iff(size(currVals,1)>size(currVals,2),currVals',currVals); + if strStartsWith(userIn,'@') % anon function + func_h = str2func(userIn); + % apply function to each cell + currVals = cellfun(@str2double,currVals, 'UniformOutput', 0); % convert from char + newVals = cellfun(func_h, currVals, 'UniformOutput', 0); + elseif any(userIn==':') % array syntax + arr = eval(userIn); + newVals = num2cell(arr); % convert to cell array + elseif any(userIn==','|userIn==';') % 2D arrays + C = strsplit(userIn, ';'); + newVals = cellfun(@(c)textscan(c, '%f',... + 'ReturnOnError', false,... + 'delimiter', {' ', ','}, 'MultipleDelimsAsOne', 1),... + C); + else % single value to copy across all cells + userIn = str2double(userIn); + newVals = num2cell(ones(size(currVals))*userIn); + end + + if length(newVals)>length(currVals) % too many new values + newVals = newVals(1:length(currVals)); % truncate new array + elseif length(newVals) 5; w = 0.5; else; w = 0.1 * w; end +% obj.UI(2).Position = [1-w 0 w 1]; +% obj.UI(1).Position = [0 0 1-w 1]; + + %%% general coordinates + pos = getpixelposition(obj.UIPanel); + borderwidth = obj.Margin; + bounds = [pos(3) pos(4)] - 2*borderwidth; + n = numel(obj.Labels); + vspace = obj.RowSpacing; + hspace = obj.ColSpacing; + rowHeight = obj.MinRowHeight + 2*vspace; + rowsPerCol = floor(bounds(2)/rowHeight); + cols = ceil((1:n)/rowsPerCol)'; + ncols = cols(end); + rows = mod(0:n - 1, rowsPerCol)' + 1; + labelColWidth = max(obj.LabelWidths) + 2*hspace; + ctrlWidthAvail = bounds(1)/ncols - labelColWidth; + ctrlColWidth = max(obj.MinCtrlWidth, min(ctrlWidthAvail, obj.MaxCtrlWidth)); + fullColWidth = labelColWidth + ctrlColWidth; + + %%% coordinates of labels + by = bounds(2) - rows*rowHeight + vspace + 1 + borderwidth; + labelPos = [vspace + (cols - 1)*fullColWidth + 1 + borderwidth... + by... + obj.LabelWidths... + repmat(rowHeight - 2*vspace, n, 1)]; + + %%% coordinates of edits + editPos = [labelColWidth + hspace + (cols - 1)*fullColWidth + 1 + borderwidth ... + by... + repmat(ctrlColWidth - 2*hspace, n, 1)... + repmat(rowHeight - 2*vspace, n, 1)]; + set(obj.Labels, {'Position'}, num2cell(labelPos, 2)); + set(obj.Controls, {'Position'}, num2cell(editPos, 2)); + + end + end + +end + diff --git a/+eui/MControl.m b/+eui/MControl.m index 5b25075d..5bad701e 100644 --- a/+eui/MControl.m +++ b/+eui/MControl.m @@ -247,10 +247,10 @@ function saveParamProfile(obj) % Called by 'Save...' button press, save a new pa end function loadParamProfile(obj, profile) + set(obj.ParamProfileLabel, 'String', 'loading...', 'ForegroundColor', [1 0 0]); % Red 'Loading...' while new set loads if ~isempty(obj.ParamEditor) - %delete existing parameters control - delete(obj.ParamEditor); - set(obj.ParamProfileLabel, 'String', 'loading...', 'ForegroundColor', [1 0 0]); % Red 'Loading...' while new set loads + % Clear existing parameters control + % TODO end factory = obj.NewExpFactory; % Find which 'world' we are in @@ -305,12 +305,18 @@ function loadParamProfile(obj, profile) paramStruct = rmfield(paramStruct, 'services'); end obj.Parameters.Struct = paramStruct; - if ~isempty(paramStruct) % Now parameters are loaded, pass to ParamEditor for display, etc. - obj.ParamEditor = eui.ParamEditor(obj.Parameters, obj.ParamPanel); % Build parameter list in Global panel by calling eui.ParamEditor - obj.ParamEditor.addlistener('Changed', @(src,~) obj.paramChanged); - if strcmp(obj.RemoteRigs.Selected.Status, 'idle') - set(obj.BeginExpButton, 'Enable', 'on') % Re-enable start button - end + if isempty(paramStruct); return; end + % Now parameters are loaded, pass to ParamEditor for display, etc. + if isempty(obj.ParamEditor) + panel = uipanel('Parent', obj.ParamPanel, 'Position', [0 0 1 1]); +% panel = uiextras.Panel('Parent', obj.ParamPanel); + obj.ParamEditor = eui.ParamEditor(obj.Parameters, panel); % Build parameter list in Global panel by calling eui.ParamEditor + else + obj.ParamEditor.buildUI(obj.Parameters); + end + obj.ParamEditor.addlistener('Changed', @(src,~) obj.paramChanged); + if strcmp(obj.RemoteRigs.Selected.Status, 'idle') + set(obj.BeginExpButton, 'Enable', 'on') % Re-enable start button end end diff --git a/+eui/ParamEditor.m b/+eui/ParamEditor.m index 75aca882..74fa16e4 100644 --- a/+eui/ParamEditor.m +++ b/+eui/ParamEditor.m @@ -1,34 +1,20 @@ classdef ParamEditor < handle - %EUI.PARAMEDITOR UI control for configuring experiment parameters - % TODO. See also EXP.PARAMETERS. - % - % Part of Rigbox - - % 2012-11 CB created - % 2017-03 MW/NS Made global panel scrollable & improved performance of - % buildGlobalUI. - % 2017-03 MW Added set values button + %UNTITLED2 Summary of this class goes here + % Detailed explanation goes here properties - GlobalVSpacing = 20 Parameters end - properties (Dependent) - Enable + properties %(Access = private) + GlobalUI + ConditionalUI + Parent + Listener end - properties (Access = private) - Root - GlobalGrid - ConditionTable - TableColumnParamNames = {} - NewConditionButton - DeleteConditionButton - MakeGlobalButton - SetValuesButton - SelectedCells %[row, column;...] of each selected cell - GlobalControls + properties (Dependent) + Enable end events @@ -36,120 +22,103 @@ end methods - function obj = ParamEditor(params, parent) - if nargin < 2 % Can call this function to display parameters is new window - parent = figure('Name', 'Parameters', 'NumberTitle', 'off',... - 'Toolbar', 'none', 'Menubar', 'none'); + function obj = ParamEditor(pars, f) + if nargin == 0; pars = []; end + if nargin < 2 + f = figure('Name', 'Parameters', 'NumberTitle', 'off',... + 'Toolbar', 'none', 'Menubar', 'none', 'DeleteFcn', @(~,~)obj.delete); end - obj.Parameters = params; - obj.build(parent); + obj.Parent = f; + obj.Listener = event.listener(f, 'SizeChanged', @(~,~)obj.onResize); + obj.GlobalUI = eui.FieldPanel(f, obj); + obj.ConditionalUI = eui.ConditionPanel(f, obj); + obj.buildUI(pars); end function delete(obj) - disp('ParamEditor destructor called'); - if obj.Root.isvalid - obj.Root.delete(); - end - end - - function value = get.Enable(obj) - value = obj.Root.Enable; + delete(obj.GlobalUI); + delete(obj.ConditionalUI); end - + function set.Enable(obj, value) - obj.Root.Enable = value; + cUI = obj.ConditionalUI; + fig = obj.Parent; + if value == true + arrayfun(@(prop) set(prop, 'Enable', 'on'), findobj(fig,'Enable','off')); + if isempty(cUI.SelectedCells) + set(cUI.MakeGlobalButton, 'Enable', 'off'); + set(cUI.DeleteConditionButton, 'Enable', 'off'); + set(cUI.SetValuesButton, 'Enable', 'off'); + end + obj.Enable = true; + else + arrayfun(@(prop) set(prop, 'Enable', 'off'), findobj(fig,'Enable','on')); + obj.Enable = false; + end end - end - - methods %(Access = protected) - function build(obj, parent) % Build parameters panel - obj.Root = uiextras.HBox('Parent', parent, 'Padding', 5, 'Spacing', 5); % Add horizontal container for Global and Conditional panels -% globalPanel = uiextras.Panel('Parent', obj.Root,... % Make 'Global' parameters panel -% 'Title', 'Global', 'Padding', 5); - globPanel = uiextras.Panel('Parent', obj.Root,... % Make 'Global' parameters panel - 'Title', 'Global', 'Padding', 5); - globalPanel = uix.ScrollingPanel('Parent', globPanel,... % Make 'Global' scroll panel - 'Padding', 5); - - obj.GlobalGrid = uiextras.Grid('Parent', globalPanel, 'Padding', 4); % Make grid for parameter fields - obj.buildGlobalUI; % Populate Global panel - globalPanel.Heights = sum(obj.GlobalGrid.RowSizes)+45; - - conditionPanel = uiextras.Panel('Parent', obj.Root,... - 'Title', 'Conditional', 'Padding', 5); % Make 'Conditional' parameters panel - conditionVBox = uiextras.VBox('Parent', conditionPanel); - obj.ConditionTable = uitable('Parent', conditionVBox,... - 'FontName', 'Consolas',... - 'RowName', [],... - 'CellEditCallback', @obj.cellEditCallback,... - 'CellSelectionCallback', @obj.cellSelectionCallback); + + function buildUI(obj, pars) + obj.Parameters = pars; + clear(obj.GlobalUI); + clear(obj.ConditionalUI); + c = obj.GlobalUI; + names = pars.GlobalNames; + for nm = names' + if strcmp(nm, 'randomiseConditions'); continue; end + if islogical(pars.Struct.(nm{:})) % If parameter is logical, make checkbox + ctrl = uicontrol('Parent', c.UIPanel, 'Style', 'checkbox', ... + 'Value', pars.Struct.(nm{:}), 'BackgroundColor', 'white'); + addField(c, nm{:}, ctrl); + else + [~, ctrl] = addField(c, nm{:}); + ctrl.String = obj.paramValue2Control(pars.Struct.(nm{:})); + end + end obj.fillConditionTable(); - conditionButtonBox = uiextras.HBox('Parent', conditionVBox); - conditionVBox.Sizes = [-1 25]; - obj.NewConditionButton = uicontrol('Parent', conditionButtonBox,... - 'Style', 'pushbutton',... - 'String', 'New condition',... - 'TooltipString', 'Add a new condition',... - 'Callback', @(~, ~) obj.newCondition()); - obj.DeleteConditionButton = uicontrol('Parent', conditionButtonBox,... - 'Style', 'pushbutton',... - 'String', 'Delete condition',... - 'TooltipString', 'Delete the selected condition',... - 'Enable', 'off',... - 'Callback', @(~, ~) obj.deleteSelectedConditions()); - obj.MakeGlobalButton = uicontrol('Parent', conditionButtonBox,... - 'Style', 'pushbutton',... - 'String', 'Globalise parameter',... - 'TooltipString', sprintf(['Make the selected condition-specific parameter global (i.e. not vary by trial)\n'... - 'This will move it to the global parameters section']),... - 'Enable', 'off',... - 'Callback', @(~, ~) obj.globaliseSelectedParameters()); - obj.SetValuesButton = uicontrol('Parent', conditionButtonBox,... - 'Style', 'pushbutton',... - 'String', 'Set values',... - 'TooltipString', 'Set selected values to specified value, range or function',... - 'Enable', 'off',... - 'Callback', @(~, ~) obj.setSelectedValues()); - - obj.Root.Sizes = [sum(obj.GlobalGrid.ColumnSizes) + 32, -1]; + obj.GlobalUI.onResize(); + %%% Special parameters + if ismember('randomiseConditions', obj.Parameters.Names) && ~pars.Struct.randomiseConditions + obj.ConditionalUI.ConditionTable.RowName = 'numbered'; + set(obj.ConditionalUI.ContextMenus(2), 'Checked', 'off'); + end end - function buildGlobalUI(obj) % Function to essemble global parameters - globalParamNames = fieldnames(obj.Parameters.assortForExperiment); % assortForExperiment divides params into global and trial-specific parameter structures - obj.GlobalControls = gobjects(length(globalParamNames),3); % Initialize object array (faster than assigning to end of array which results in two calls to constructor) - for i=1:length(globalParamNames) % using for loop (sorry Chris!) to populate object array 2017-02-14 MW - [obj.GlobalControls(i,1), obj.GlobalControls(i,2), obj.GlobalControls(i,3)]... % [editors, labels, buttons] - = obj.addParamUI(globalParamNames{i}); + function setRandomized(obj, value) + % If randomiseConditions doesn't exist and new value is false, add + % the parameter and set it to false + if ~ismember('randomiseConditions', obj.Parameters.Names) && value == false + description = 'Whether to randomise the conditional paramters or present them in order'; + obj.Parameters.set('randomiseConditions', false, description, 'logical') + elseif ismember('randomiseConditions', obj.Parameters.Names) + obj.update('randomiseConditions', logical(value)); + end + menu = obj.ConditionalUI.ContextMenus(2); + if value == false + obj.ConditionalUI.ConditionTable.RowName = 'numbered'; + menu.Checked = 'off'; + else + obj.ConditionalUI.ConditionTable.RowName = []; + menu.Checked = 'on'; end - % Above code replaces the following as after 2014a, MATLAB doesn't no - % longer uses numrical handles but instead uses object arrays -% [editors, labels, buttons] = cellfun(... -% @(n) obj.addParamUI(n), fieldnames(globalParams), 'UniformOutput', false); -% editors = cell2mat(editors); -% labels = cell2mat(labels); -% buttons = cell2mat(buttons); -% obj.GlobalControls = [labels, editors, buttons]; -% obj.GlobalGrid.Children = obj.GlobalControls(:); - -% obj.GlobalGrid.Children = -% blah = cat(1,obj.GlobalControls(:,1),obj.GlobalControls(:,2),obj.GlobalControls(:,3)); -% Doesn't work for some reason - MW 2017-02-15 - - child_handles = allchild(obj.GlobalGrid); % Get child handles for GlobalGrid - child_handles = [child_handles(end-1:-3:1); child_handles(end:-3:1); child_handles(end-2:-3:1)]; % Reorder them so all labels come first, then ctrls, then buttons -% child_handles = [child_handles(2:3:end); child_handles(3:3:end); child_handles(1:3:end)]; % Reorder them so all labels come first, then ctrls, then buttons - obj.GlobalGrid.Contents = child_handles; % Set children to new order - % uistack - - obj.GlobalGrid.ColumnSizes = [180, 200, 40]; % Set column sizes - obj.GlobalGrid.Spacing = 1; - obj.GlobalGrid.RowSizes = repmat(obj.GlobalVSpacing, 1, size(obj.GlobalControls, 1)); end -% function swapConditions(obj, idx1, idx2) % Function started, never -% finished - MW 2017-02-15 -% % params = obj.Parameters.trial -% end + function fillConditionTable(obj) + % Build the condition table + titles = obj.Parameters.TrialSpecificNames; + [~, trialParams] = obj.Parameters.assortForExperiment; + if isempty(titles) + obj.ConditionalUI.ButtonPanel.Visible = 'off'; + obj.ConditionalUI.UIPanel.Visible = 'off'; + obj.GlobalUI.UIPanel.Position(3) = 1; + else + obj.ConditionalUI.ButtonPanel.Visible = 'on'; + obj.ConditionalUI.UIPanel.Visible = 'on'; + data = reshape(struct2cell(trialParams), numel(titles), [])'; + data = mapToCell(@(e) obj.paramValue2Control(e), data); + set(obj.ConditionalUI.ConditionTable, 'ColumnName', titles, 'Data', data,... + 'ColumnEditable', true(1, numel(titles))); + end + end function addEmptyConditionToParam(obj, name) assert(obj.Parameters.isTrialSpecific(name),... @@ -183,217 +152,134 @@ function addEmptyConditionToParam(obj, name) obj.Parameters.Struct.(name) = cat(2, obj.Parameters.Struct.(name), newValue); end - function cellSelectionCallback(obj, src, eventData) - obj.SelectedCells = eventData.Indices; - if size(eventData.Indices, 1) > 0 - %cells selected, enable buttons - set(obj.MakeGlobalButton, 'Enable', 'on'); - set(obj.DeleteConditionButton, 'Enable', 'on'); - set(obj.SetValuesButton, 'Enable', 'on'); + function newValue = update(obj, name, value, row) + % FIXME change name to updateGlobal + if nargin < 4; row = 1; end + currValue = obj.Parameters.Struct.(name)(:,row); + if iscell(currValue) + % cell holders are allowed to be different types of value + newValue = obj.controlValue2Param(currValue{1}, value, true); + obj.Parameters.Struct.(name){:,row} = newValue; else - %nothing selected, disable buttons - set(obj.MakeGlobalButton, 'Enable', 'off'); - set(obj.DeleteConditionButton, 'Enable', 'off'); - set(obj.SetValuesButton, 'Enable', 'off'); + newValue = obj.controlValue2Param(currValue, value); + obj.Parameters.Struct.(name)(:,row) = newValue; end + notify(obj, 'Changed'); end - function newCondition(obj) - disp('adding new condition row'); - cellfun(@obj.addEmptyConditionToParam, obj.Parameters.TrialSpecificNames); - obj.fillConditionTable(); - end - - function deleteSelectedConditions(obj) - %DELETESELECTEDCONDITIONS Removes the selected conditions from table - % The callback for the 'Delete condition' button. This removes the - % selected conditions from the table and if less than two conditions - % remain, globalizes them. - % TODO: comment function better, index in a clearer fashion + function globaliseParamAtCell(obj, name, row) + % Make parameter 'name' a global parameter and set it's value to be + % that of the specified row. % - % See also EXP.PARAMETERS, GLOBALISESELECTEDPARAMETERS - rows = unique(obj.SelectedCells(:,1)); - % If the number of remaining conditions is 1 or less... - names = obj.Parameters.TrialSpecificNames; - numConditions = size(obj.Parameters.Struct.(names{1}),2); - if numConditions-length(rows) <= 1 - remainingIdx = find(all(1:numConditions~=rows,1)); - if isempty(remainingIdx); remainingIdx = 1; end - % change selected cells to be all fields (except numRepeats which - % is assumed to always be the last column) - obj.SelectedCells =[ones(length(names)-1,1)*remainingIdx, (1:length(names)-1)']; - %... globalize them - obj.globaliseSelectedParameters; - obj.Parameters.removeConditions(rows) -% for i = 1:numel(names) -% newValue = iff(any(remainingIdx), obj.Struct.(names{i})(:,remainingIdx), obj.Struct.(names{i})(1)); -% % If the parameter is Num repeats, set the value -% if strcmp(names{i}, 'numRepeats') -% obj.Struct.(names{i}) = newValue; -% else -% obj.makeGlobal(names{i}, newValue); -% end -% end - else % Otherwise delete the selected conditions as usual - obj.Parameters.removeConditions(rows); - end - obj.fillConditionTable(); %refresh the table of conditions - end - - function globaliseSelectedParameters(obj) - [cols, iu] = unique(obj.SelectedCells(:,2)); - names = obj.TableColumnParamNames(cols); - rows = obj.SelectedCells(iu,1); %get rows of unique selected cols - arrayfun(@obj.globaliseParamAtCell, rows, cols); - obj.fillConditionTable(); %refresh the table of conditions - %now add global controls for parameters - newGlobals = gobjects(length(names),3); % Initialize object array (faster than assigning to end of array which results in two calls to constructor) - for i=length(names):-1:1 % using for loop (sorry Chris!) to initialize and populate object array 2017-02-15 MW - [newGlobals(i,1), newGlobals(i,2), newGlobals(i,3)]... % [editors, labels, buttons] - = obj.addParamUI(names{i}); - end - -% [editors, labels, buttons] = arrayfun(@obj.addParamUI, names); % -% 2017-02-15 MW can no longer use arrayfun with object outputs - idx = size(obj.GlobalControls, 1); % Calculate number of current Global params - new = numel(newGlobals); - obj.GlobalControls = [obj.GlobalControls; newGlobals]; % Add new globals to object - ggHandles = obj.GlobalGrid.Contents; - ggHandles = [ggHandles(1:idx); ggHandles((end-new+2):3:end);... - ggHandles(idx+1:idx*2); ggHandles((end-new+1):3:end);... - ggHandles(idx*2+1:idx*3); ggHandles((end-new+3):3:end)]; % Reorder them so all labels come first, then ctrls, then buttons - obj.GlobalGrid.Contents = ggHandles; % Set children to new order - - % Reset sizes - obj.GlobalGrid.RowSizes = repmat(obj.GlobalVSpacing, 1, size(obj.GlobalControls, 1)); - set(get(obj.GlobalGrid, 'Parent'),... - 'Heights', sum(obj.GlobalGrid.RowSizes)+45); % Reset height of globalPanel - obj.GlobalGrid.ColumnSizes = [180, 200, 40]; - obj.GlobalGrid.Spacing = 1; - end - - function globaliseParamAtCell(obj, row, col) - name = obj.TableColumnParamNames{col}; + % See also EXP.PARAMETERS/MAKEGLOBAL, UI.CONDITIONPANEL/MAKEGLOBAL value = obj.Parameters.Struct.(name)(:,row); obj.Parameters.makeGlobal(name, value); - end - - function setSelectedValues(obj) % Set multiple fields in conditional table - disp('updating table cells'); - cols = obj.SelectedCells(:,2); % selected columns - uCol = unique(obj.SelectedCells(:,2)); - rows = obj.SelectedCells(:,1); % selected rows - % get current values of selected cells - currVals = arrayfun(@(u)obj.ConditionTable.Data(rows(cols==u),u), uCol, 'UniformOutput', 0); - names = obj.TableColumnParamNames(uCol); % selected column names - promt = cellfun(@(a,b) [a ' (' num2str(sum(cols==b)) ')'],... - names, num2cell(uCol), 'UniformOutput', 0); % names of columns & num selected rows - defaultans = cellfun(@(c) c(1), currVals); - answer = inputdlg(promt,'Set values', 1, cellflat(defaultans)); % prompt for input - if isempty(answer) % if user presses cancel - return + % Refresh the table of conditions + obj.fillConditionTable; + % Add new global parameter to field panel + if islogical(value) % If parameter is logical, make checkbox + ctrl = uicontrol('Parent', obj.GlobalUI.UIPanel, 'Style', 'checkbox', ... + 'Value', value, 'BackgroundColor', 'white'); + addField(obj.GlobalUI, name, ctrl); + else + [~, ctrl] = addField(obj.GlobalUI, name); + ctrl.String = obj.paramValue2Control(value); end - % set values for each column - cellfun(@(a,b,c) setNewVals(a,b,c), answer, currVals, names, 'UniformOutput', 0); - function newVals = setNewVals(userIn, currVals, paramName) - % check array orientation - currVals = iff(size(currVals,1)>size(currVals,2),currVals',currVals); - if strStartsWith(userIn,'@') % anon function - func_h = str2func(userIn); - % apply function to each cell - currVals = cellfun(@str2double,currVals, 'UniformOutput', 0); % convert from char - newVals = cellfun(func_h, currVals, 'UniformOutput', 0); - elseif any(userIn==':') % array syntax - arr = eval(userIn); - newVals = num2cell(arr); % convert to cell array - elseif any(userIn==','|userIn==';') % 2D arrays - C = strsplit(userIn, ';'); - newVals = cellfun(@(c)textscan(c, '%f',... - 'ReturnOnError', false,... - 'delimiter', {' ', ','}, 'MultipleDelimsAsOne', 1),... - C); - else % single value to copy across all cells - userIn = str2double(userIn); - newVals = num2cell(ones(size(currVals))*userIn); - end - - if length(newVals)>length(currVals) % too many new values - newVals = newVals(1:length(currVals)); % truncate new array - elseif length(newVals) gUIExtent && cUIExtent > obj.ConditionalUI.MinWidth + % If global UI controls are cut off and there is no dead space in + % the table but the minimum table width hasn't been reached, reduce + % the conditional UI width: table has scroll bar and global panel + % does not + % FIXME calculate how much space required for min control width +% obj.GlobalUI.MinCtrlWidth + % Calculate conditional UI width in normalized units + requiredWidth = (cUI.Position(3) / cUIExtent) * (colExtent - gUIExtent); + minConditionalWidth = (cUI.Position(3) / cUIExtent) * obj.ConditionalUI.MinWidth; + if requiredWidth < minConditionalWidth + % If the required width is smaller that the minimum table width, + % use minimum table width + cUI.Position(3) = minConditionalWidth; + else % Otherwise use this width + cUI.Position(3) = requiredWidth; + end + cUI.Position(1) = 1-cUI.Position(3); + gUI.Position(3) = 1-cUI.Position(3); + elseif extent(3) < 1 && colWidth < obj.GlobalUI.MaxCtrlWidth + % If there is dead table space and the global UI columns are cut + % off or squashed, reduce the conditional panel + cUI.Position(3) = cUI.Position(3) - (panelWidth - (panelWidth * extent(3))); + cUI.Position(1) = cUI.Position(1) + (panelWidth - (panelWidth * extent(3))); + gUI.Position(3) = cUI.Position(1); + elseif extent(3) < 1 && colExtent < gUIExtent + % Plenty of space! Increase conditional UI a bit + deadspace = gUIExtent - colExtent; % Spece between panels in pixels + % Convert global UI pixels to relative units + gUI.Position(3) = (gUI.Position(3) / gUIExtent) * (gUIExtent - (deadspace/2)); + cUI.Position(1) = gUI.Position(3); + cUI.Position(3) = 1-gUI.Position(3); + elseif extent(3) >= 1 && colExtent < gUIExtent + % If the table space is cut off and there is dead space in the + % global UI panel, reduce the global UI panel + % If the extra space is minimum, return + if floor(gUIExtent - colExtent) <= 2; return; end + deadspace = gUIExtent - colExtent; % Spece between panels in pixels + gUI.Position(3) = (gUI.Position(3) / gUIExtent) * (gUIExtent - deadspace); + cUI.Position(3) = 1-gUI.Position(3); + cUI.Position(1) = gUI.Position(3); else - newParam = obj.controlValue2Param(currValue, eventData.NewData); - obj.Parameters.Struct.(paramName)(:,row) = newParam; - end - % if successful update the cell with default formatting - data = get(src, 'Data'); - reformed = obj.paramValue2Control(newParam); - if iscell(reformed) - % the reformed data type is a cell, this should be a one element - % wrapping cell - if numel(reformed) == 1 - reformed = reformed{1}; - else - error('Cannot handle data reformatted data type'); - end + % Compromise by having both panels take up half the figure +% [cUI.Position([1,3]), gUI.Position(3)] = deal(0.5); end - data{row,col} = reformed; - set(src, 'Data', data); - %notify listeners of change - notify(obj, 'Changed'); + notify(obj.ConditionalUI.ButtonPanel, 'SizeChanged'); end - - function updateGlobal(obj, param, src) - currParamValue = obj.Parameters.Struct.(param); - switch get(src, 'style') - case 'checkbox' - newValue = logical(get(src, 'value')); - obj.Parameters.Struct.(param) = newValue; - case 'edit' - newValue = obj.controlValue2Param(currParamValue, get(src, 'string')); - obj.Parameters.Struct.(param) = newValue; - % if successful update the control with default formatting and - % modified colour - set(src, 'String', obj.paramValue2Control(newValue),... - 'ForegroundColor', [1 0 0]); %red indicating it has changed - %notify listeners of change - notify(obj, 'Changed'); + end + + methods (Static) + function data = paramValue2Control(data) + % convert from parameter value to control value, i.e. a value class + % that can be easily displayed and edited by the user. Everything + % except logicals are converted to charecter arrays. + switch class(data) + case 'function_handle' + % convert a function handle to it's string name + data = func2str(data); + case 'logical' + data = data ~= 0; % If logical do nothing, basically. + case 'string' + data = char(data); % Strings not allowed in condition table data + otherwise + if isnumeric(data) + % format numeric types as string number list + strlist = mapToCell(@num2str, data); + data = strJoin(strlist, ', '); + elseif iscellstr(data) + data = strJoin(data, ', '); + end end - end - - function [data, paramNames, titles] = tableData(obj) - [~, trialParams] = obj.Parameters.assortForExperiment; - paramNames = fieldnames(trialParams); - titles = obj.Parameters.title(paramNames); - data = reshape(struct2cell(trialParams), numel(paramNames), [])'; - data = mapToCell(@(e) obj.paramValue2Control(e), data); + % all other data types stay as they are end - function data = controlValue2Param(obj, currParam, data, allowTypeChange) + function data = controlValue2Param(currParam, data, allowTypeChange) % Convert the values displayed in the UI ('control values') to % parameter values. String representations of numrical arrays and % functions are converted back to their 'native' classes. @@ -432,111 +318,7 @@ function updateGlobal(obj, param, src) end end end - - function data = paramValue2Control(obj, data) - % convert from parameter value to control value, i.e. a value class - % that can be easily displayed and edited by the user. Everything - % except logicals are converted to charecter arrays. - switch class(data) - case 'function_handle' - % convert a function handle to it's string name - data = func2str(data); - case 'logical' - data = data ~= 0; % If logical do nothing, basically. - case 'string' - data = char(data); % Strings not allowed in condition table data - otherwise - if isnumeric(data) - % format numeric types as string number list - strlist = mapToCell(@num2str, data); - data = strJoin(strlist, ', '); - elseif iscellstr(data) - data = strJoin(data, ', '); - end - end - % all other data types stay as they are - end - - function fillConditionTable(obj) - [data, params, titles] = obj.tableData; - set(obj.ConditionTable, 'ColumnName', titles, 'Data', data,... - 'ColumnEditable', true(1, numel(titles))); - obj.TableColumnParamNames = params; - end - - function makeTrialSpecific(obj, paramName, ctrls) - [uirow, ~] = find(obj.GlobalControls == ctrls{1}); - assert(numel(uirow) == 1, 'Unexpected number of matching global controls'); - cellfun(@(c) delete(c), ctrls); - obj.GlobalControls(uirow,:) = []; - obj.GlobalGrid.RowSizes(uirow) = []; - obj.Parameters.makeTrialSpecific(paramName); - obj.fillConditionTable(); - set(get(obj.GlobalGrid, 'Parent'),... - 'Heights', sum(obj.GlobalGrid.RowSizes)+45); % Reset height of globalPanel - end - - function [ctrl, label, buttons] = addParamUI(obj, name) % Adds ui element for each parameter - parent = obj.GlobalGrid; % Made by build function above - ctrl = []; - label = []; - buttons = []; - if iscell(name) % 2017-02-14 MW function now called with arrayFun (instead of cellFun) - name = name{1,1}; - end - value = obj.paramValue2Control(obj.Parameters.Struct.(name)); % convert from parameter value to control value (everything but logical values become strings) - title = obj.Parameters.title(name); - description = obj.Parameters.description(name); - - if islogical(value) % If parameter is logical, make checkbox - for i = 1:length(value) - ctrl(end+1) = uicontrol('Parent', parent,... - 'Style', 'checkbox',... - 'TooltipString', description,... - 'Value', value(i),... % Added 2017-02-15 MW set checkbox to what ever the parameter value is - 'Callback', @(src, e) obj.updateGlobal(name, src)); - end - elseif ischar(value) - ctrl = uicontrol('Parent', parent,... - 'BackgroundColor', [1 1 1],... - 'Style', 'edit',... - 'String', value,... - 'TooltipString', description,... - 'UserData', name,... % save the name of the parameter in userdata - 'HorizontalAlignment', 'left',... - 'Callback', @(src, e) obj.updateGlobal(name, src)); -% elseif iscellstr(value) -% lines = mkStr(value, [], sprintf('\n'), []); -% ctrl = uicontrol('Parent', parent,... -% 'BackgroundColor', [1 1 1],... -% 'Style', 'edit',... -% 'Max', 2,... %make it multiline -% 'String', lines,... -% 'TooltipString', description,... -% 'HorizontalAlignment', 'left',... -% 'UserData', name,... % save the name of the parameter in userdata -% 'Callback', @(src, e) obj.updateGlobal(name, src)); - end - if ~isempty(ctrl) % If control box is made, add label and conditional button - label = uicontrol('Parent', parent,... - 'Style', 'text', 'String', title, 'HorizontalAlignment', 'left',... - 'TooltipString', description); % Why not use bui.label? MW 2017-02-15 - bbox = uiextras.HBox('Parent', parent); % Make HBox for button - % UIContainer no longer present in GUILayoutToolbox, it used to - % call uipanel with the following args: - % 'Units', 'Normalized'; 'BorderType', 'none') -% buttons = bbox.UIContainer; - buttons = uicontrol('Parent', bbox, 'Style', 'pushbutton',... % Make 'conditional parameter' button - 'String', '[...]',... - 'TooltipString', sprintf(['Make this a condition parameter (i.e. vary by trial).\n'... - 'This will move it to the trial conditions table.']),... - 'FontSize', 7,... - 'Callback', @(~,~) obj.makeTrialSpecific(name, {ctrl, label, bbox})); - bbox.Sizes = 29; % Resize button height to 29px - end - end end - end diff --git a/+eui/ParamEditor_old.m b/+eui/ParamEditor_old.m new file mode 100644 index 00000000..05855aef --- /dev/null +++ b/+eui/ParamEditor_old.m @@ -0,0 +1,516 @@ +classdef ParamEditor < handle + %EUI.PARAMEDITOR UI control for configuring experiment parameters + % TODO. See also EXP.PARAMETERS. + % + % Part of Rigbox + + % 2012-11 CB created + % 2017-03 MW/NS Made global panel scrollable & improved performance of + % buildGlobalUI. + % 2017-03 MW Added set values button + + properties + GlobalVSpacing = 20 + Parameters + end + + properties (Dependent) + Enable + end + + properties (Access = private) + Root + GlobalGrid + ConditionTable + TableColumnParamNames = {} + NewConditionButton + DeleteConditionButton + MakeGlobalButton + SetValuesButton + SelectedCells %[row, column;...] of each selected cell + GlobalControls + end + + events + Changed + end + + methods + function obj = ParamEditor(params, parent) + if nargin < 2 % Can call this function to display parameters is new window + parent = figure('Name', 'Parameters', 'NumberTitle', 'off',... + 'Toolbar', 'none', 'Menubar', 'none'); + end + obj.Parameters = params; + obj.build(parent); + end + + function delete(obj) + disp('ParamEditor destructor called'); + if obj.Root.isvalid + obj.Root.delete(); + end + end + + function value = get.Enable(obj) + value = obj.Root.Enable; + end + + function set.Enable(obj, value) + obj.Root.Enable = value; + end + end + + methods %(Access = protected) + function build(obj, parent) % Build parameters panel + obj.Root = uiextras.HBox('Parent', parent, 'Padding', 5, 'Spacing', 5); % Add horizontal container for Global and Conditional panels +% globalPanel = uiextras.Panel('Parent', obj.Root,... % Make 'Global' parameters panel +% 'Title', 'Global', 'Padding', 5); + globPanel = uiextras.Panel('Parent', obj.Root,... % Make 'Global' parameters panel + 'Title', 'Global', 'Padding', 5); + globalPanel = uix.ScrollingPanel('Parent', globPanel,... % Make 'Global' scroll panel + 'Padding', 5); + + obj.GlobalGrid = uiextras.Grid('Parent', globalPanel, 'Padding', 4); % Make grid for parameter fields + obj.buildGlobalUI; % Populate Global panel + globalPanel.Heights = sum(obj.GlobalGrid.RowSizes)+45; + + conditionPanel = uiextras.Panel('Parent', obj.Root,... + 'Title', 'Conditional', 'Padding', 5); % Make 'Conditional' parameters panel + conditionVBox = uiextras.VBox('Parent', conditionPanel); + obj.ConditionTable = uitable('Parent', conditionVBox,... + 'FontName', 'Consolas',... + 'RowName', [],... + 'CellEditCallback', @obj.cellEditCallback,... + 'CellSelectionCallback', @obj.cellSelectionCallback); + obj.fillConditionTable(); + conditionButtonBox = uiextras.HBox('Parent', conditionVBox); + conditionVBox.Sizes = [-1 25]; + obj.NewConditionButton = uicontrol('Parent', conditionButtonBox,... + 'Style', 'pushbutton',... + 'String', 'New condition',... + 'TooltipString', 'Add a new condition',... + 'Callback', @(~, ~) obj.newCondition()); + obj.DeleteConditionButton = uicontrol('Parent', conditionButtonBox,... + 'Style', 'pushbutton',... + 'String', 'Delete condition',... + 'TooltipString', 'Delete the selected condition',... + 'Enable', 'off',... + 'Callback', @(~, ~) obj.deleteSelectedConditions()); + obj.MakeGlobalButton = uicontrol('Parent', conditionButtonBox,... + 'Style', 'pushbutton',... + 'String', 'Globalise parameter',... + 'TooltipString', sprintf(['Make the selected condition-specific parameter global (i.e. not vary by trial)\n'... + 'This will move it to the global parameters section']),... + 'Enable', 'off',... + 'Callback', @(~, ~) obj.globaliseSelectedParameters()); + obj.SetValuesButton = uicontrol('Parent', conditionButtonBox,... + 'Style', 'pushbutton',... + 'String', 'Set values',... + 'TooltipString', 'Set selected values to specified value, range or function',... + 'Enable', 'off',... + 'Callback', @(~, ~) obj.setSelectedValues()); + + obj.Root.Sizes = [sum(obj.GlobalGrid.ColumnSizes) + 32, -1]; + end + + function buildGlobalUI(obj) % Function to essemble global parameters + globalParamNames = fieldnames(obj.Parameters.assortForExperiment); % assortForExperiment divides params into global and trial-specific parameter structures + obj.GlobalControls = gobjects(length(globalParamNames),3); % Initialize object array (faster than assigning to end of array which results in two calls to constructor) + for i=1:length(globalParamNames) % using for loop (sorry Chris!) to populate object array 2017-02-14 MW + [obj.GlobalControls(i,1), obj.GlobalControls(i,2), obj.GlobalControls(i,3)]... % [editors, labels, buttons] + = obj.addParamUI(globalParamNames{i}); + end + + child_handles = allchild(obj.GlobalGrid); % Get child handles for GlobalGrid + child_handles = [child_handles(end-1:-3:1); child_handles(end:-3:1); child_handles(end-2:-3:1)]; % Reorder them so all labels come first, then ctrls, then buttons + obj.GlobalGrid.Contents = child_handles; % Set children to new order + + obj.GlobalGrid.ColumnSizes = [180, 200, 40]; % Set column sizes + obj.GlobalGrid.Spacing = 1; + obj.GlobalGrid.RowSizes = repmat(obj.GlobalVSpacing, 1, size(obj.GlobalControls, 1)); + end + +% function swapConditions(obj, idx1, idx2) % Function started, never +% finished - MW 2017-02-15 +% % params = obj.Parameters.trial +% end + + function addEmptyConditionToParam(obj, name) + assert(obj.Parameters.isTrialSpecific(name),... + 'Tried to add a new condition to global parameter ''%s''', name); + % work out what the right 'empty' is for the parameter + currValue = obj.Parameters.Struct.(name); + if isnumeric(currValue) + newValue = zeros(size(currValue, 1), 1, class(currValue)); + elseif islogical(currValue) + newValue = false(size(currValue, 1), 1); + elseif iscell(currValue) + if numel(currValue) > 0 + if iscellstr(currValue) + % if all elements are strings, default to a blank string + newValue = {''}; + elseif isa(currValue{1}, 'function_handle') + % first element is a function handle, so create with a @nop + % handle + newValue = {@nop}; + else + % misc cell case - default to empty element + newValue = {[]}; + end + else + % misc case - default to empty element + newValue = {[]}; + end + else + error('Adding empty condition for ''%s'' type not implemented', class(currValue)); + end + obj.Parameters.Struct.(name) = cat(2, obj.Parameters.Struct.(name), newValue); + end + + function cellSelectionCallback(obj, src, eventData) + obj.SelectedCells = eventData.Indices; + if size(eventData.Indices, 1) > 0 + %cells selected, enable buttons + set(obj.MakeGlobalButton, 'Enable', 'on'); + set(obj.DeleteConditionButton, 'Enable', 'on'); + set(obj.SetValuesButton, 'Enable', 'on'); + else + %nothing selected, disable buttons + set(obj.MakeGlobalButton, 'Enable', 'off'); + set(obj.DeleteConditionButton, 'Enable', 'off'); + set(obj.SetValuesButton, 'Enable', 'off'); + end + end + + function newCondition(obj) + disp('adding new condition row'); + cellfun(@obj.addEmptyConditionToParam, obj.Parameters.TrialSpecificNames); + obj.fillConditionTable(); + end + + function deleteSelectedConditions(obj) + %DELETESELECTEDCONDITIONS Removes the selected conditions from table + % The callback for the 'Delete condition' button. This removes the + % selected conditions from the table and if less than two conditions + % remain, globalizes them. + % TODO: comment function better, index in a clearer fashion + % + % See also EXP.PARAMETERS, GLOBALISESELECTEDPARAMETERS + rows = unique(obj.SelectedCells(:,1)); + % If the number of remaining conditions is 1 or less... + names = obj.Parameters.TrialSpecificNames; + numConditions = size(obj.Parameters.Struct.(names{1}),2); + if numConditions-length(rows) <= 1 + remainingIdx = find(all(1:numConditions~=rows,1)); + if isempty(remainingIdx); remainingIdx = 1; end + % change selected cells to be all fields (except numRepeats which + % is assumed to always be the last column) + obj.SelectedCells =[ones(length(names)-1,1)*remainingIdx, (1:length(names)-1)']; + %... globalize them + obj.globaliseSelectedParameters; + obj.Parameters.removeConditions(rows) + else % Otherwise delete the selected conditions as usual + obj.Parameters.removeConditions(rows); + end + obj.fillConditionTable(); %refresh the table of conditions + end + + function globaliseSelectedParameters(obj) + [cols, iu] = unique(obj.SelectedCells(:,2)); + names = obj.TableColumnParamNames(cols); + rows = obj.SelectedCells(iu,1); %get rows of unique selected cols + arrayfun(@obj.globaliseParamAtCell, rows, cols); + obj.fillConditionTable(); %refresh the table of conditions + %now add global controls for parameters + newGlobals = gobjects(length(names),3); % Initialize object array (faster than assigning to end of array which results in two calls to constructor) + for i=length(names):-1:1 % using for loop (sorry Chris!) to initialize and populate object array 2017-02-15 MW + [newGlobals(i,1), newGlobals(i,2), newGlobals(i,3)]... % [editors, labels, buttons] + = obj.addParamUI(names{i}); + end + + idx = size(obj.GlobalControls, 1); % Calculate number of current Global params + new = numel(newGlobals); + obj.GlobalControls = [obj.GlobalControls; newGlobals]; % Add new globals to object + ggHandles = obj.GlobalGrid.Contents; + ggHandles = [ggHandles(1:idx); ggHandles((end-new+2):3:end);... + ggHandles(idx+1:idx*2); ggHandles((end-new+1):3:end);... + ggHandles(idx*2+1:idx*3); ggHandles((end-new+3):3:end)]; % Reorder them so all labels come first, then ctrls, then buttons + obj.GlobalGrid.Contents = ggHandles; % Set children to new order + + % Reset sizes + obj.GlobalGrid.RowSizes = repmat(obj.GlobalVSpacing, 1, size(obj.GlobalControls, 1)); + set(get(obj.GlobalGrid, 'Parent'),... + 'Heights', sum(obj.GlobalGrid.RowSizes)+45); % Reset height of globalPanel + obj.GlobalGrid.ColumnSizes = [180, 200, 40]; + obj.GlobalGrid.Spacing = 1; + end + + function globaliseParamAtCell(obj, row, col) + name = obj.TableColumnParamNames{col}; + value = obj.Parameters.Struct.(name)(:,row); + obj.Parameters.makeGlobal(name, value); + end + + function setSelectedValues(obj) % Set multiple fields in conditional table + disp('updating table cells'); + cols = obj.SelectedCells(:,2); % selected columns + uCol = unique(obj.SelectedCells(:,2)); + rows = obj.SelectedCells(:,1); % selected rows + % get current values of selected cells + currVals = arrayfun(@(u)obj.ConditionTable.Data(rows(cols==u),u), uCol, 'UniformOutput', 0); + names = obj.TableColumnParamNames(uCol); % selected column names + promt = cellfun(@(a,b) [a ' (' num2str(sum(cols==b)) ')'],... + names, num2cell(uCol), 'UniformOutput', 0); % names of columns & num selected rows + defaultans = cellfun(@(c) c(1), currVals); + answer = inputdlg(promt,'Set values', 1, cellflat(defaultans)); % prompt for input + if isempty(answer) % if user presses cancel + return + end + % set values for each column + cellfun(@(a,b,c) setNewVals(a,b,c), answer, currVals, names, 'UniformOutput', 0); + function newVals = setNewVals(userIn, currVals, paramName) + % check array orientation + currVals = iff(size(currVals,1)>size(currVals,2),currVals',currVals); + if strStartsWith(userIn,'@') % anon function + func_h = str2func(userIn); + % apply function to each cell + currVals = cellfun(@str2double,currVals, 'UniformOutput', 0); % convert from char + newVals = cellfun(func_h, currVals, 'UniformOutput', 0); + elseif any(userIn==':') % array syntax + arr = eval(userIn); + newVals = num2cell(arr); % convert to cell array + elseif any(userIn==','|userIn==';') % 2D arrays + C = strsplit(userIn, ';'); + newVals = cellfun(@(c)textscan(c, '%f',... + 'ReturnOnError', false,... + 'delimiter', {' ', ','}, 'MultipleDelimsAsOne', 1),... + C); + else % single value to copy across all cells + userIn = str2double(userIn); + newVals = num2cell(ones(size(currVals))*userIn); + end + + if length(newVals)>length(currVals) % too many new values + newVals = newVals(1:length(currVals)); % truncate new array + elseif length(newVals) 1) ||... % Number of rows > 1 for chars - (~ischar(obj.pStruct.(n)) && size(obj.pStruct.(n), 2) > 1); % Number of columns > 1 for all others + ~strcmp(n, 'randomiseConditions') &&... % randomiseConditions always global + ((ischar(obj.pStruct.(n)) && size(obj.pStruct.(n), 1) > 1) ||... % Number of rows > 1 for chars + (~ischar(obj.pStruct.(n)) && size(obj.pStruct.(n), 2) > 1)); % Number of columns > 1 for all others for i = 1:n name = obj.pNames{i}; obj.IsTrialSpecific.(name) = isTrialSpecificDefault(name); @@ -182,8 +182,8 @@ function makeGlobal(obj, name, newValue) 'UniformOutput', false); % concatenate trial parameter trialParamValues = cat(1, trialParamValues{:}); - if isempty(trialParamValues) - trialParamValues = {1}; + if isempty(trialParamValues) % Removed MW 30.01.19 + trialParamValues = {}; end trialParams = cell2struct(trialParamValues, trialParamNames, 1)'; globalParams = cell2struct(globalParamValues, globalParamNames, 1); diff --git a/cortexlab/+git/update.m b/cortexlab/+git/update.m index 200ba269..cd939a83 100644 --- a/cortexlab/+git/update.m +++ b/cortexlab/+git/update.m @@ -28,7 +28,7 @@ function update(scheduled) % than an hour ago. if (scheduled && (weekday(now) ~= scheduled) && now - lastFetch < 7) || ... (scheduled && (weekday(now) == scheduled) && now - lastFetch < 1) || ... - (~scheduled && now - lastFetch < 1/24) + (~scheduled && now - lastFetch < 1/24) return end disp('Updating code...') diff --git a/signals b/signals index 93520307..85072177 160000 --- a/signals +++ b/signals @@ -1 +1 @@ -Subproject commit 93520307c1cb1a9dc12fa8bda685a01b9ab80401 +Subproject commit 850721777f0f4cd27c6a5248cf83aa246b53c9a2 From 04a8a3d9b0971cc31d87e397beacc02b5c70efcb Mon Sep 17 00:00:00 2001 From: k1o0 Date: Tue, 5 Feb 2019 12:51:16 +0200 Subject: [PATCH 014/108] Updates for GUILT compatibility --- +eui/ConditionPanel.m | 28 +++++++++++++++------------- +eui/FieldPanel.m | 3 ++- +eui/MControl.m | 2 +- +eui/ParamEditor.m | 18 ++++++++++++------ signals | 2 +- 5 files changed, 31 insertions(+), 22 deletions(-) diff --git a/+eui/ConditionPanel.m b/+eui/ConditionPanel.m index 7c4b89fe..eacd317f 100644 --- a/+eui/ConditionPanel.m +++ b/+eui/ConditionPanel.m @@ -29,8 +29,7 @@ methods function obj = ConditionPanel(f, ParamEditor, varargin) obj.ParamEditor = ParamEditor; - obj.UIPanel = uipanel('Parent', f, 'BorderType', 'none',... - 'BackgroundColor', 'white', 'Position', [0.5 0.05 0.5 0.95]); + obj.UIPanel = uix.VBox('Parent', f, 'BackgroundColor', 'white'); % Create a child menu for the uiContextMenus c = uicontextmenu; obj.UIPanel.UIContextMenu = c; @@ -41,7 +40,8 @@ obj.ContextMenus(3) = uimenu(c, 'Label', 'Sort by selected column', ... 'MenuSelectedFcn', @(~,~)disp('feature not yet implemented'), 'Tag', 'sort by'); % Create condition table - obj.ConditionTable = uitable('Parent', obj.UIPanel,... + p = uix.Panel('Parent', obj.UIPanel); + obj.ConditionTable = uitable('Parent', p,... 'FontName', 'Consolas',... 'RowName', [],... 'RearrangeableColumns', true,... @@ -51,14 +51,14 @@ 'CellEditCallback', @obj.onEdit,... 'CellSelectionCallback', @obj.onSelect); % Create button panel to hold condition control buttons - obj.ButtonPanel = uipanel('BackgroundColor', 'white',... - 'Position', [0.5 0 0.5 0.05], 'BorderType', 'none'); + obj.ButtonPanel = uix.HBox('Parent', obj.UIPanel, ... + 'BackgroundColor', 'white'); % Create callback so that width of button panel is slave to width of % conditional UIPanel - b = obj.ButtonPanel; - fcn = @(s)set(obj.ButtonPanel, 'Position', ... - [s.Position(1) b.Position(2) s.Position(3) b.Position(4)]); - obj.Listener = event.listener(obj.UIPanel, 'SizeChanged', @(s,~)fcn(s)); +% b = obj.ButtonPanel; +% fcn = @(s)set(obj.ButtonPanel, 'Position', ... +% [s.Position(1) b.Position(2) s.Position(3) b.Position(4)]); +% obj.Listener = event.listener(obj.UIPanel, 'SizeChanged', @(s,~)fcn(s)); % Define some common properties props.BackgroundColor = 'white'; props.Style = 'pushbutton'; @@ -67,28 +67,30 @@ % Create out four buttons obj.NewConditionButton = uicontrol(props,... 'String', 'New condition',... - 'Position',[0 0 1/4 1],... + ...'Position',[0 0 1/4 1],... 'TooltipString', 'Add a new condition',... 'Callback', @(~, ~) obj.newCondition()); obj.DeleteConditionButton = uicontrol(props,... 'String', 'Delete condition',... - 'Position',[1/4 0 1/4 1],... + ...'Position',[1/4 0 1/4 1],... 'TooltipString', 'Delete the selected condition',... 'Enable', 'off',... 'Callback', @(~, ~) obj.deleteSelectedConditions()); obj.MakeGlobalButton = uicontrol(props,... 'String', 'Globalise parameter',... - 'Position',[2/4 0 1/4 1],... + ...'Position',[2/4 0 1/4 1],... 'TooltipString', sprintf(['Make the selected condition-specific parameter global (i.e. not vary by trial)\n'... 'This will move it to the global parameters section']),... 'Enable', 'off',... 'Callback', @(~, ~) obj.makeGlobal()); obj.SetValuesButton = uicontrol(props,... 'String', 'Set values',... - 'Position',[3/4 0 1/4 1],... + ...'Position',[3/4 0 1/4 1],... 'TooltipString', 'Set selected values to specified value, range or function',... 'Enable', 'off',... 'Callback', @(~, ~) obj.setSelectedValues()); + obj.ButtonPanel.Widths = [-1 -1 -1 -1]; + obj.UIPanel.Heights = [-1 25]; end function onEdit(obj, src, eventData) diff --git a/+eui/FieldPanel.m b/+eui/FieldPanel.m index 33ae7ee1..10980cb6 100644 --- a/+eui/FieldPanel.m +++ b/+eui/FieldPanel.m @@ -28,7 +28,8 @@ methods function obj = FieldPanel(f, ParamEditor, varargin) obj.ParamEditor = ParamEditor; - obj.UIPanel = uipanel('Parent', f, 'BorderType', 'none',... + p = uix.Panel('Parent', f); + obj.UIPanel = uipanel('Parent', p, 'BorderType', 'none',... 'BackgroundColor', 'white', 'Position', [0 0 0.5 1]); obj.Listener = event.listener(obj.UIPanel, 'SizeChanged', @obj.onResize); end diff --git a/+eui/MControl.m b/+eui/MControl.m index 5bad701e..69c8628d 100644 --- a/+eui/MControl.m +++ b/+eui/MControl.m @@ -250,7 +250,7 @@ function loadParamProfile(obj, profile) set(obj.ParamProfileLabel, 'String', 'loading...', 'ForegroundColor', [1 0 0]); % Red 'Loading...' while new set loads if ~isempty(obj.ParamEditor) % Clear existing parameters control - % TODO + clear(obj.ParamEditor) end factory = obj.NewExpFactory; % Find which 'world' we are in diff --git a/+eui/ParamEditor.m b/+eui/ParamEditor.m index 74fa16e4..4c375b7e 100644 --- a/+eui/ParamEditor.m +++ b/+eui/ParamEditor.m @@ -7,6 +7,7 @@ end properties %(Access = private) + UIPanel GlobalUI ConditionalUI Parent @@ -27,11 +28,12 @@ if nargin < 2 f = figure('Name', 'Parameters', 'NumberTitle', 'off',... 'Toolbar', 'none', 'Menubar', 'none', 'DeleteFcn', @(~,~)obj.delete); + obj.Listener = event.listener(f, 'SizeChanged', @(~,~)obj.onResize); end obj.Parent = f; - obj.Listener = event.listener(f, 'SizeChanged', @(~,~)obj.onResize); - obj.GlobalUI = eui.FieldPanel(f, obj); - obj.ConditionalUI = eui.ConditionPanel(f, obj); + obj.UIPanel = uix.HBox('Parent', f); + obj.GlobalUI = eui.FieldPanel(obj.UIPanel, obj); + obj.ConditionalUI = eui.ConditionPanel(obj.UIPanel, obj); obj.buildUI(pars); end @@ -57,10 +59,14 @@ function delete(obj) end end - function buildUI(obj, pars) - obj.Parameters = pars; + function clear(obj) clear(obj.GlobalUI); clear(obj.ConditionalUI); + end + + function buildUI(obj, pars) + obj.Parameters = pars; + obj.clear() c = obj.GlobalUI; names = pars.GlobalNames; for nm = names' @@ -75,12 +81,12 @@ function buildUI(obj, pars) end end obj.fillConditionTable(); - obj.GlobalUI.onResize(); %%% Special parameters if ismember('randomiseConditions', obj.Parameters.Names) && ~pars.Struct.randomiseConditions obj.ConditionalUI.ConditionTable.RowName = 'numbered'; set(obj.ConditionalUI.ContextMenus(2), 'Checked', 'off'); end + obj.GlobalUI.onResize(); end function setRandomized(obj, value) diff --git a/signals b/signals index 85072177..23f93fb3 160000 --- a/signals +++ b/signals @@ -1 +1 @@ -Subproject commit 850721777f0f4cd27c6a5248cf83aa246b53c9a2 +Subproject commit 23f93fb365c441d803e7ff43b5d8f17801a409e9 From e47f4624469a9935d8c77e8372ad005447b6a597 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Fri, 8 Feb 2019 11:46:53 +0200 Subject: [PATCH 015/108] New infer params --- +exp/inferParameters.m | 33 ++++++--------------------------- signals | 2 +- 2 files changed, 7 insertions(+), 28 deletions(-) diff --git a/+exp/inferParameters.m b/+exp/inferParameters.m index 945845d1..50009901 100644 --- a/+exp/inferParameters.m +++ b/+exp/inferParameters.m @@ -18,34 +18,16 @@ expdef = which(func2str(expdef)); end -net = sig.Net; -e = struct; -e.t = net.origin('t'); -e.events = net.subscriptableOrigin('events'); -e.pars = net.subscriptableOrigin('pars'); -e.pars.CacheSubscripts = true; -e.visual = net.subscriptableOrigin('visual'); -e.audio.Devices = @dummyDev; -e.inputs = net.subscriptableOrigin('inputs'); -e.outputs = net.subscriptableOrigin('outputs'); +e = sig.void; +pars = sig.void(true); +audio.Devices = @dummyDev; try + expdeffun(e.t, e.events, pars, e.visual, e.inputs, e.outputs, audio); - expdeffun(e.t, e.events, e.pars, e.visual, e.inputs, e.outputs, e.audio); - - % paramNames will be the strings corresponding to the fields of e.pars + % paramNames will be the strings corresponding to the fields of pars % that the user tried to reference in her expdeffun. - paramNames = e.pars.Subscripts.keys'; - %The paramValues are signals corresponding to those parameters and they - %will all be empty, except when they've been given explicit numerical - %definitions right at the end of the function - and in that case, we'll - %take those values (extracted into matlab datatypes, from the signals, - %using .Node.CurrValue) to be the desired default values. - paramValues = e.pars.Subscripts.values'; - parsStruct = cell2struct(cell(size(paramNames)), paramNames); - for i = 1:size(paramNames,1) - parsStruct.(paramNames{i}) = paramValues{i}.Node.CurrValue; - end + parsStruct = pars.Subscripts; sz = iff(isempty(fieldnames(parsStruct)), 1,... % if there are no paramters sz = 1 structfun(@(a)size(a,2), parsStruct)); % otherwise get number of columns isChar = structfun(@ischar, parsStruct); % we disregard charecter arrays @@ -60,12 +42,9 @@ ExpPanel_fn = [path filesep ExpPanel_name ext]; if exist(ExpPanel_fn,'file'); parsStruct.expPanelFun = ExpPanel_name; end catch ex - net.delete(); rethrow(ex) end -net.delete(); - function dev = dummyDev(~) % Returns a dummy audio device structure, regardless of input % Returns a standard structure with values for generating tone diff --git a/signals b/signals index 93520307..c81b99e0 160000 --- a/signals +++ b/signals @@ -1 +1 @@ -Subproject commit 93520307c1cb1a9dc12fa8bda685a01b9ab80401 +Subproject commit c81b99e0922dbab9ab5d9a9770ff476c96fb6126 From 0f4bff25239bf0f9d96431f07f432b5e520d97e1 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Fri, 8 Feb 2019 12:44:53 +0200 Subject: [PATCH 016/108] Check for reserved params --- +exp/inferParameters.m | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/+exp/inferParameters.m b/+exp/inferParameters.m index 50009901..7e1a937e 100644 --- a/+exp/inferParameters.m +++ b/+exp/inferParameters.m @@ -5,12 +5,6 @@ % create some signals just to pass to the definition function and track % which parameter names are used -% if ischar(expdef) && file.exists(expdef) -% expdeffun = fileFunction(expdef); -% else -% expdeffun = expdef; -% expdef = which(func2str(expdef)); -% end if ischar(expdef) && file.exists(expdef) expdeffun = fileFunction(expdef); else @@ -28,6 +22,14 @@ % paramNames will be the strings corresponding to the fields of pars % that the user tried to reference in her expdeffun. parsStruct = pars.Subscripts; + + % Check for reserved fieldnames + reserved = {'randomiseConditions', 'services', 'expPanelFun', ... + 'numRepeats', 'defFunction', 'waterType', 'isPassive'}; + assert(~any(ismember(fieldnames(parsStruct), reserved)), ... + 'Lord have mercy, the following param names are reserved:\n%s', ... + strjoin(intersect(fieldnames(parsStruct), reserved), ', ')) + sz = iff(isempty(fieldnames(parsStruct)), 1,... % if there are no paramters sz = 1 structfun(@(a)size(a,2), parsStruct)); % otherwise get number of columns isChar = structfun(@ischar, parsStruct); % we disregard charecter arrays From c5a2e9821a77ad0a8750a196d04d46e3605e0a9e Mon Sep 17 00:00:00 2001 From: k1o0 Date: Fri, 8 Feb 2019 13:39:56 +0200 Subject: [PATCH 017/108] No more multiple default aud dev names --- +hw/devices.m | 4 ++-- signals | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/+hw/devices.m b/+hw/devices.m index 2734fe92..cdf5f493 100644 --- a/+hw/devices.m +++ b/+hw/devices.m @@ -69,8 +69,8 @@ % Get list of audio devices devs = getOr(rig, 'audioDevices', PsychPortAudio('GetDevices')); % Sanitize the names - names = matlab.lang.makeValidName([{'default'} {devs(2:end).DeviceName}],... - 'ReplacementStyle', 'delete'); + names = matlab.lang.makeValidName({devs.DeviceName}, 'ReplacementStyle', 'delete'); + names = iff(ismember('defaut', names), names, @()[{'default'} names(2:end)]); for i = 1:length(names); devs(i).DeviceName = names{i}; end rig.audioDevices = devs; end diff --git a/signals b/signals index 93520307..c81b99e0 160000 --- a/signals +++ b/signals @@ -1 +1 @@ -Subproject commit 93520307c1cb1a9dc12fa8bda685a01b9ab80401 +Subproject commit c81b99e0922dbab9ab5d9a9770ff476c96fb6126 From 571ee1844a46ed90b82835ab27930cbc7e5f15c7 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Fri, 8 Feb 2019 14:28:40 +0200 Subject: [PATCH 018/108] Bug fix for numRepeats when there's signal char param --- +exp/inferParameters.m | 7 +++---- signals | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/+exp/inferParameters.m b/+exp/inferParameters.m index 7e1a937e..3c44117c 100644 --- a/+exp/inferParameters.m +++ b/+exp/inferParameters.m @@ -30,10 +30,9 @@ 'Lord have mercy, the following param names are reserved:\n%s', ... strjoin(intersect(fieldnames(parsStruct), reserved), ', ')) + szFcn = @(a)iff(ischar(a), @()size(a,1), @()size(a,2)); sz = iff(isempty(fieldnames(parsStruct)), 1,... % if there are no paramters sz = 1 - structfun(@(a)size(a,2), parsStruct)); % otherwise get number of columns - isChar = structfun(@ischar, parsStruct); % we disregard charecter arrays - if any(isChar); sz = sz(~isChar); end + structfun(szFcn, parsStruct)); % otherwise get number of columns % add 'numRepeats' parameter, where total number of trials = 1000 parsStruct.numRepeats = ones(1,max(sz))*floor(1000/max(sz)); parsStruct.defFunction = expdef; @@ -56,4 +55,4 @@ 'DefaultSampleRate', 44100,... 'NrOutputChannels', 2); end -end \ No newline at end of file +end diff --git a/signals b/signals index c81b99e0..8a56f9e6 160000 --- a/signals +++ b/signals @@ -1 +1 @@ -Subproject commit c81b99e0922dbab9ab5d9a9770ff476c96fb6126 +Subproject commit 8a56f9e6b3b5cf0d37c34e5b0b948c2e55bb45d8 From 6e19d6126595524949e0fe0c6e9d33e89a9ce884 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Fri, 8 Feb 2019 15:39:51 +0200 Subject: [PATCH 019/108] Subjects list disabled during login --- +eui/AlyxPanel.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/+eui/AlyxPanel.m b/+eui/AlyxPanel.m index f8b6da14..7abcff6e 100644 --- a/+eui/AlyxPanel.m +++ b/+eui/AlyxPanel.m @@ -217,6 +217,8 @@ function login(obj) % Logging out does not cause the token to expire, instead the % token is simply deleted from this object. + % Temporarily disable the Subject Selector + obj.NewExpSubject.UIControl.Enable = 'off'; % Reset headless flag in case user wishes to retry connection obj.AlyxInstance.Headless = false; % Are we logging in or out? @@ -282,6 +284,8 @@ function login(obj) notify(obj, 'Disconnected'); % Notify listeners of logout obj.log('Logged out of Alyx'); end + % Reable the Subject Selector + obj.NewExpSubject.UIControl.Enable = 'on'; obj.dispWaterReq() end From 8522c13d1c58d286acc1dd29c347444322b31621 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Sun, 10 Feb 2019 21:15:15 +0000 Subject: [PATCH 020/108] Field change to water-requirement endpoint --- +eui/AlyxPanel.m | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/+eui/AlyxPanel.m b/+eui/AlyxPanel.m index f8b6da14..aade462f 100644 --- a/+eui/AlyxPanel.m +++ b/+eui/AlyxPanel.m @@ -601,13 +601,13 @@ function viewSubjectHistory(obj, ax) axWater = axes('Parent',plotBox); plot(axWater, dates, obj.round([records.given_water_total], 'up'), '.-'); hold(axWater, 'on'); - plot(axWater, dates, obj.round([records.given_water_hydrogel], 'down'), '.-'); - plot(axWater, dates, obj.round([records.given_water_liquid], 'down'), '.-'); + plot(axWater, dates, obj.round([records.given_water_supplement], 'down'), '.-'); + plot(axWater, dates, obj.round([records.given_water_reward], 'down'), '.-'); plot(axWater, dates, obj.round([records.expected_water], 'up'), 'r', 'LineWidth', 2.0); box(axWater, 'off'); xlim(axWater, [min(dates) maxDate]); set(axWater, 'XTickLabel', arrayfun(@(x)datestr(x, 'dd-mmm'), get(axWater, 'XTick'), 'uni', false)) - ylabel(axWater, 'water/hydrogel (mL)'); + ylabel(axWater, 'water (mL)'); % Create table of useful weight and water information, % sorted by date @@ -627,14 +627,14 @@ function viewSubjectHistory(obj, ax) arrayfun(@(x)iff(isnan(x), [], @()sprintf('%.1f', 0.8*(x-iw)+iw)), expected', 'uni', false), ... weightPctByDate'); waterDat = (... - num2cell(horzcat([records.given_water_liquid]', [records.given_water_hydrogel]', ... + num2cell(horzcat([records.given_water_reward]', [records.given_water_supplement]', ... [records.given_water_total]', [records.expected_water]',... [records.given_water_total]'-[records.expected_water]'))); waterDat = cellfun(@(x)sprintf('%.2f', x), waterDat, 'uni', false); waterDat(~[records.is_water_restricted],[1,3]) = {'ad lib'}; dat = horzcat(dat, waterDat); - set(histTable, 'ColumnName', {'date', 'meas. weight', '80% weight', 'weight pct', 'water', 'hydrogel', 'total', 'min water', 'excess'}, ... + set(histTable, 'ColumnName', {'date', 'meas. weight', '80% weight', 'weight pct', 'water', 'supplement', 'total', 'min water', 'excess'}, ... 'Data', dat(end:-1:1,:),... 'ColumnEditable', false(1,5)); histbox.Widths = [ -1 725]; From 210c8486bc26b96d5fe8ee1ebecca8262c0e789c Mon Sep 17 00:00:00 2001 From: k1o0 Date: Sun, 10 Feb 2019 22:39:48 +0000 Subject: [PATCH 021/108] Pagination support --- alyx-matlab | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alyx-matlab b/alyx-matlab index 6fc933b9..13264858 160000 --- a/alyx-matlab +++ b/alyx-matlab @@ -1 +1 @@ -Subproject commit 6fc933b99bec09689a83b024284e8023d2c5793d +Subproject commit 132648581fe7ff7be9136baa00cdefe2cbb67c2f From b5ea66e02e52d4025c7af63367be79be326d138a Mon Sep 17 00:00:00 2001 From: kevin-j-miller Date: Fri, 15 Feb 2019 15:57:28 +0000 Subject: [PATCH 022/108] Modify Alyx login to use newid instead of inputdlg. This makes the 'enter' key on the keyboard synonymous with the 'ok' button --- +eui/AlyxPanel.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/+eui/AlyxPanel.m b/+eui/AlyxPanel.m index 78f9c781..ef221f98 100644 --- a/+eui/AlyxPanel.m +++ b/+eui/AlyxPanel.m @@ -318,7 +318,7 @@ function giveFutureWater(obj) 'enter space-separated numbers, i.e. \n',... '[tomorrow, day after that, day after that.. etc] \n\n',... 'Enter "0" to skip a day\nEnter "-1" to indicate training for that day\n']); - amtStr = inputdlg(prompt,'Future Amounts', [1 50]); + amtStr = newid(prompt,'Future Amounts', [1 50]); if isempty(amtStr)||~obj.AlyxInstance.IsLoggedIn return % user pressed 'Close' or 'x' end @@ -450,10 +450,10 @@ function recordWeight(obj, weight, subject) dlgTitle = 'Manual weight logging'; numLines = 1; defaultAns = {'',''}; - weight = inputdlg(prompt, dlgTitle, numLines, defaultAns); + weight = newid(prompt, dlgTitle, numLines, defaultAns); if isempty(weight); return; end end - % inputdlg returns weight as a cell, otherwise it may now be + % newid returns weight as a cell, otherwise it may now be weight = ensureCell(weight); % ensure it's a cell % convert to double if weight is a string weight = iff(ischar(weight{1}), str2double(weight{1}), weight{1}); From e9c2f5e3e7aa46c4f96a0ba501faa777624dd0df Mon Sep 17 00:00:00 2001 From: kevin-j-miller Date: Fri, 15 Feb 2019 15:57:40 +0000 Subject: [PATCH 023/108] Modify Alyx login to use newid instead of inputdlg. This makes the 'enter' key on the keyboard synonymous with the 'ok' button --- alyx-matlab | 2 +- cortexlab/+git/changes.m | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 cortexlab/+git/changes.m diff --git a/alyx-matlab b/alyx-matlab index 13264858..77a077ea 160000 --- a/alyx-matlab +++ b/alyx-matlab @@ -1 +1 @@ -Subproject commit 132648581fe7ff7be9136baa00cdefe2cbb67c2f +Subproject commit 77a077ead0c34ba3acdfb007e8da9ca40243547c diff --git a/cortexlab/+git/changes.m b/cortexlab/+git/changes.m deleted file mode 100644 index d9e9f013..00000000 --- a/cortexlab/+git/changes.m +++ /dev/null @@ -1,6 +0,0 @@ -disp('Updating queued Alyx posts...') -posts = dirPlus(getOr(dat.paths, 'localAlyxQueue', 'C:/localAlyxQueue')); -posts = posts(endsWith(posts, 'put')); -newPosts = cellfun(@(str)[str(1:end-3) 'patch'], posts, 'uni', 0); -status = cellfun(@movefile, posts, newPosts); -assert(all(status), 'Unable to rename queued Alyx files, please do this manually') \ No newline at end of file From 5f2fb584ee3c8fc9c70058ddf8fba4bbc5380cac Mon Sep 17 00:00:00 2001 From: kevin-j-miller Date: Fri, 15 Feb 2019 17:11:00 +0000 Subject: [PATCH 024/108] Expose the AlyxPanel property of the MControl object, so that the login method can be invoked from the command line. I want to do this so that I can put a batch file on the desktop of the computer that runs MC to help streamline the workflow. --- +eui/MControl.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/+eui/MControl.m b/+eui/MControl.m index 5b25075d..9d5c201c 100644 --- a/+eui/MControl.m +++ b/+eui/MControl.m @@ -17,6 +17,7 @@ properties LoggingDisplay % control for showing log output + AlyxPanel % holds the AlyxPanel object (see buildUI(), eui.AlyxPanel()) end properties (SetAccess = private) @@ -33,7 +34,6 @@ properties (Access = private) ParamEditor ParamPanel - AlyxPanel % holds the AlyxPanel object (see buildUI(), eui.AlyxPanel()) BeginExpButton % The 'Start' button that begins an experiment RigOptionsButton % The 'Options' button that opens the rig options dialog NewExpFactory % A struct containing all availiable experiment types and function handles to constructors for their default parameters From 9b56e4f80de5b49a3f686a2aa5066c00ebfd1155 Mon Sep 17 00:00:00 2001 From: kevin-j-miller Date: Mon, 18 Feb 2019 14:17:51 +0000 Subject: [PATCH 025/108] modified alyx-matlab with more convenient textboxes and desktop shortcuts for common use-cases --- alyx-matlab | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alyx-matlab b/alyx-matlab index 77a077ea..c5c5c1ba 160000 --- a/alyx-matlab +++ b/alyx-matlab @@ -1 +1 @@ -Subproject commit 77a077ead0c34ba3acdfb007e8da9ca40243547c +Subproject commit c5c5c1ba22dce86549ed56fd257c3852f5949391 From 64416dd363e42c0936d2d170c8672adba1945b78 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Tue, 19 Feb 2019 22:35:11 +0200 Subject: [PATCH 026/108] Bug fix for turning empty objects into struct --- cb-tools/obj2struct.m | 8 +++++--- tests/obj2json_test.m | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 tests/obj2json_test.m diff --git a/cb-tools/obj2struct.m b/cb-tools/obj2struct.m index 60e91eb6..67937832 100644 --- a/cb-tools/obj2struct.m +++ b/cb-tools/obj2struct.m @@ -23,12 +23,15 @@ end s = obj2struct(m); else % Normal object + s.ClassContructor = class(obj); % Supply class name for loading object names = fieldnames(obj); % Get list of public properties for i = 1:length(names) - if isobject(obj.(names{i})) % Property contains an object + if isempty(obj) % Object and therefore all properties are empty + s.(names{i}) = []; + elseif isobject(obj.(names{i})) % Property contains an object if startsWith(class(obj.(names{i})),'daq.ni.') % Do not attempt to save ni daq sessions of channels - s.(names{i}) = []; + s.(names{i}) = []; else % Recurse s.(names{i}) = obj2struct(obj.(names{i})); end @@ -55,7 +58,6 @@ s.(names{i}) = obj.(names{i}); end end - s.ClassContructor = class(obj); % Supply class name for loading object end elseif iscell(obj) % If dealing with cell array, recurse through elements diff --git a/tests/obj2json_test.m b/tests/obj2json_test.m new file mode 100644 index 00000000..e26dd2cf --- /dev/null +++ b/tests/obj2json_test.m @@ -0,0 +1,23 @@ +%% Test obj2struct with given data +data = struct; +data.A = struct(... % Scalar struct + 'field1', zeros(10), ... + 'field2', true(10), ... + 'field3', pi, ... + 'field4', single(10), ... + 'field5', '10'); +data.B = hw.DaqController(); % Obj containing empty obj +v = daq.getVendors(); +if v(strcmp({v.ID},'ni')).IsOperational + data.B.createDaqChannels(); % Add daq.ni obj +end +data.C = struct; % Non-scalar struct +data.C(1,1).a = 1; +data.C(2,1).a = 2; +data.C(1,2).a = 3; +data.C(2,2).a = 4; +data.D = @(a,b,c)zeros(c,b,a); % Function handle + +json = obj2json(data); +out = '{"A":{"field1":[[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0]],"field2":[[true,true,true,true,true,true,true,true,true,true],[true,true,true,true,true,true,true,true,true,true],[true,true,true,true,true,true,true,true,true,true],[true,true,true,true,true,true,true,true,true,true],[true,true,true,true,true,true,true,true,true,true],[true,true,true,true,true,true,true,true,true,true],[true,true,true,true,true,true,true,true,true,true],[true,true,true,true,true,true,true,true,true,true],[true,true,true,true,true,true,true,true,true,true],[true,true,true,true,true,true,true,true,true,true]],"field3":3.1415926535897931,"field4":10,"field5":"10"},"B":{"ClassContructor":"hw.DaqController","ChannelNames":[],"SignalGenerators":{"ClassContructor":"hw.PulseSwitcher","OpenValue":[],"ClosedValue":[],"ParamsFun":[],"DefaultCommand":[],"DefaultValue":[]},"DaqIds":"Dev1","DaqChannelIds":[],"SampleRate":1000,"DaqSession":[],"DigitalDaqSession":[],"Value":[],"NumChannels":0,"AnalogueChannelsIdx":[]},"C":[[{"a":1},{"a":3}],[{"a":2},{"a":4}]],"D":"@(a,b,c)zeros(c,b,a)"}'; +assert(strcmp(json,out), 'Test failed') \ No newline at end of file From 4fda5c090a63f98180a7c8752e81520333a58f18 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Wed, 20 Feb 2019 00:38:50 +0200 Subject: [PATCH 027/108] Added error msgID and tests --- +exp/inferParameters.m | 3 +- tests/expDefinitions/advancedChoiceWorld.m | 195 ++++++ .../advancedChoiceWorldExpPanel.m | 1 + .../advancedChoiceWorld_parameters.mat | Bin 0 -> 729 bytes tests/expDefinitions/choiceWorld.m | 636 ++++++++++++++++++ .../expDefinitions/choiceWorld_parameters.mat | Bin 0 -> 709 bytes tests/inferParamsPerformanceTest.m | 21 + tests/inferParamsTest.m | 65 ++ 8 files changed, 920 insertions(+), 1 deletion(-) create mode 100644 tests/expDefinitions/advancedChoiceWorld.m create mode 100644 tests/expDefinitions/advancedChoiceWorldExpPanel.m create mode 100644 tests/expDefinitions/advancedChoiceWorld_parameters.mat create mode 100644 tests/expDefinitions/choiceWorld.m create mode 100644 tests/expDefinitions/choiceWorld_parameters.mat create mode 100644 tests/inferParamsPerformanceTest.m create mode 100644 tests/inferParamsTest.m diff --git a/+exp/inferParameters.m b/+exp/inferParameters.m index 3c44117c..275c964f 100644 --- a/+exp/inferParameters.m +++ b/+exp/inferParameters.m @@ -27,7 +27,8 @@ reserved = {'randomiseConditions', 'services', 'expPanelFun', ... 'numRepeats', 'defFunction', 'waterType', 'isPassive'}; assert(~any(ismember(fieldnames(parsStruct), reserved)), ... - 'Lord have mercy, the following param names are reserved:\n%s', ... + 'exp:InferParameters:ReservedParameters', ... + 'The following param names are reserved:\n%s', ... strjoin(intersect(fieldnames(parsStruct), reserved), ', ')) szFcn = @(a)iff(ischar(a), @()size(a,1), @()size(a,2)); diff --git a/tests/expDefinitions/advancedChoiceWorld.m b/tests/expDefinitions/advancedChoiceWorld.m new file mode 100644 index 00000000..836ef546 --- /dev/null +++ b/tests/expDefinitions/advancedChoiceWorld.m @@ -0,0 +1,195 @@ +function advancedChoiceWorld(t, evts, p, vs, in, out, audio) +%% advancedChoiceWorld +% Burgess 2AUFC task with contrast discrimination and baited equal contrast +% trial conditions. +% 2017-03-25 Added contrast discrimination MW +% 2017-08 Added baited trials (thanks PZH) +% 2017-09-26 Added manual reward key presses +% 2017-10-26 p.wheelGain now in mm/deg units +% 2018-03-15 Added time sampler function for delays + +%% parameters +wheel = in.wheelMM; % The wheel input in mm turned tangential to the surface +rewardKey = p.rewardKey.at(evts.expStart); % get value of rewardKey at experiemnt start, otherwise it will take the same value each new trial +rewardKeyPressed = in.keyboard.strcmp(rewardKey); % true each time the reward key is pressed +contrastLeft = p.stimulusContrast(1); +contrastRight = p.stimulusContrast(2); + +%% when to present stimuli & allow visual stim to move +% stimulus should come on after the wheel has been held still for the +% duration of the preStimulusDelay. The quiescence threshold is a tenth of +% the rotary encoder resolution. +preStimulusDelay = p.preStimulusDelay.map(@timeSampler).at(evts.newTrial); % at(evts.newTrial) fix for rig pre-delay +stimulusOn = sig.quiescenceWatch(preStimulusDelay, t, wheel, 10); +interactiveDelay = p.interactiveDelay.map(@timeSampler); +interactiveOn = stimulusOn.delay(interactiveDelay); % the closed-loop period starts when the stimulus comes on, plus an 'interactive delay' + +audioDevice = audio.Devices('default'); +onsetToneSamples = p.onsetToneAmplitude*... + mapn(p.onsetToneFrequency, 0.1, audioDevice.DefaultSampleRate,... + 0.02, audioDevice.NrOutputChannels, @aud.pureTone); % aud.pureTone(freq, duration, samprate, "ramp duration", nAudChannels) +audio.default = onsetToneSamples.at(interactiveOn); % At the time of 'interative on', send samples to audio device and log as 'onsetTone' + +%% wheel position to stimulus displacement +% Here we define the multiplication factor for changing the wheel signal +% into mm/deg visual angle units. The Lego wheel used has a 31mm radius. +% The standard KBLER rotary encoder uses X4 encoding; we record all edges +% (up and down) from both channels for maximum resolution. This means that +% e.g. a KBLER 2400 with 100 pulses per revolution will actually generate +% *400* position ticks per full revolution. +wheelOrigin = wheel.at(interactiveOn); % wheel position sampled at 'interactiveOn' +stimulusDisplacement = p.wheelGain*(wheel - wheelOrigin); % yoke the stimulus displacment to the wheel movement during closed loop + +%% define response and response threshold +responseTimeOver = (t - t.at(interactiveOn)) > p.responseWindow; % p.responseWindow may be set to Inf +threshold = interactiveOn.setTrigger(... + abs(stimulusDisplacement) >= abs(p.stimulusAzimuth) | responseTimeOver); + +response = cond(... + responseTimeOver, 0,... % if the response time is over the response = 0 + true, -sign(stimulusDisplacement)); % otherwise it should be the inverse of the sign of the stimulusDisplacement + +response = response.at(threshold); % only update the response signal when the threshold has been crossed +stimulusOff = threshold.delay(1); % true a second after the threshold is crossed + +%% define correct response and feedback +% each trial randomly pick -1 or 1 value for use in baited (guess) trials +rndDraw = map(evts.newTrial, @(x) sign(rand(x)-0.5)); +correctResponse = cond(contrastLeft > contrastRight, -1,... % contrast left + contrastLeft < contrastRight, 1,... % contrast right + (contrastLeft + contrastRight == 0), 0,... % no-go (zero contrast) + (contrastLeft == contrastRight) & (rndDraw < 0), -1,... % equal contrast (baited) + (contrastLeft == contrastRight) & (rndDraw > 0), 1); % equal contrast (baited) +feedback = correctResponse == response; +% Only update the feedback signal at the time of the threshold being crossed +feedback = feedback.at(threshold).delay(0.1); + +noiseBurstSamples = p.noiseBurstAmp*... + mapn(audioDevice.NrOutputChannels, p.noiseBurstDur*audioDevice.DefaultSampleRate, @randn); +audio.default = noiseBurstSamples.at(feedback==0); % When the subject gives an incorrect response, send samples to audio device and log as 'noiseBurst' + +reward = merge(rewardKeyPressed, feedback > 0);% only update when feedback changes to greater than 0, or reward key is pressed +out.reward = p.rewardSize.at(reward); % output this signal to the reward controller + +%% stimulus azimuth +azimuth = cond(... + stimulusOn.to(interactiveOn), 0,... % Before the closed-loop condition, the stimulus is at it's starting azimuth + interactiveOn.to(threshold), stimulusDisplacement,... % Closed-loop condition, where the azimuth yoked to the wheel + threshold.to(stimulusOff), -response*abs(p.stimulusAzimuth)); % Once threshold is reached the stimulus is fixed again + +%% define the visual stimulus + +% Test stim left +leftStimulus = vis.grating(t, 'sinusoid', 'gaussian'); % create a Gabor grating +leftStimulus.orientation = p.stimulusOrientation(1); +leftStimulus.altitude = 0; +leftStimulus.sigma = [9,9]; % in visual degrees +leftStimulus.spatialFreq = p.spatialFrequency; % in cylces per degree +leftStimulus.phase = 2*pi*evts.newTrial.map(@(v)rand); % phase randomly changes each trial +leftStimulus.contrast = contrastLeft; +leftStimulus.azimuth = -p.stimulusAzimuth + azimuth; +% When show is true, the stimulus is visible +leftStimulus.show = stimulusOn.to(stimulusOff); + +vs.leftStimulus = leftStimulus; % store stimulus in visual stimuli set and log as 'leftStimulus' + +% Test stim right +rightStimulus = vis.grating(t, 'sinusoid', 'gaussian'); +rightStimulus.orientation = p.stimulusOrientation(2); +rightStimulus.altitude = 0; +rightStimulus.sigma = [9,9]; +rightStimulus.spatialFreq = p.spatialFrequency; +rightStimulus.phase = 2*pi*evts.newTrial.map(@(v)rand); +rightStimulus.contrast = contrastRight; +rightStimulus.azimuth = p.stimulusAzimuth + azimuth; +rightStimulus.show = stimulusOn.to(stimulusOff); + +vs.rightStimulus = rightStimulus; % store stimulus in visual stimuli set + +%% End trial and log events +% Let's use the next set of conditional paramters only if positive feedback +% was given, or if the parameter 'Repeat incorrect' was set to false. +nextCondition = feedback > 0 | p.repeatIncorrect == false; + +% we want to save these signals so we put them in events with appropriate +% names: +evts.stimulusOn = stimulusOn; +evts.preStimulusDelay = preStimulusDelay; +% save the contrasts as a difference between left and right +evts.contrast = p.stimulusContrast.map(@diff); +evts.contrastLeft = contrastLeft; +evts.contrastRight = contrastRight; +evts.azimuth = azimuth; +evts.response = response; +evts.feedback = feedback; +evts.interactiveOn = interactiveOn; +% Accumulate reward signals and append microlitre units +evts.totalReward = out.reward.scan(@plus, 0).map(fun.partial(@sprintf, '%.1fl')); + +% Trial ends when evts.endTrial updates. +% If the value of evts.endTrial is false, the current set of conditional +% parameters are used for the next trial, if evts.endTrial updates to true, +% the next set of randowmly picked conditional parameters is used +evts.endTrial = nextCondition.at(stimulusOff).delay(p.interTrialDelay.map(@timeSampler)); + +%% Parameter defaults +% See timeSampler for full details on what values the *Delay paramters can +% take. Conditional perameters are defined as having ncols > 1, where each +% column is a condition. All conditional paramters must have the same +% number of columns. +try +%%% Contrast starting set +% C = [1 0;0 1;0.5 0;0 0.5]'; +%%% Contrast discrimination set +% c = [1 0.5 0.25 0.12 0.06 0]; +% c = combvec(c, c); +% C = unique([c, flipud(c)]', 'rows')'; +%%% Contrast detection set +c = [1 0.5 0.25 0.12 0.06 0]; +C = [c, zeros(1, numel(c)-1); zeros(1, numel(c)-1), c]; +%%% +p.stimulusContrast = C; + +p.repeatIncorrect = abs(diff(C,1)) > 0.25; % | all(C==0); +p.onsetToneFrequency = 5000; +p.interactiveDelay = 0.4; +p.onsetToneAmplitude = 0.15; +p.responseWindow = Inf; +p.stimulusAzimuth = 90; +p.noiseBurstAmp = 0.01; +p.noiseBurstDur = 0.5; +p.rewardSize = 3; +p.rewardKey = 'r'; +p.stimulusOrientation = [0, 0]'; +p.spatialFrequency = 0.19; % Prusky & Douglas, 2004 +p.interTrialDelay = 0.5; +p.wheelGain = 5; +p.preStimulusDelay = [0 0.1 0.09]'; +catch % ex +% disp(getReport(ex, 'extended', 'hyperlinks', 'on')) +end + +%% Helper functions +function duration = timeSampler(time) +% TIMESAMPLER Sample a time from some distribution +% If time is a single value, duration is that value. If time = [min max], +% then duration is sampled uniformally. If time = [min, max, time const], +% then duration is sampled from a exponential distribution, giving a flat +% hazard rate. If numel(time) > 3, duration is a randomly sampled value +% from time. +% +% See also exp.TimeSampler + if nargin == 0; duration = 0; return; end + switch length(time) + case 3 % A time sampled with a flat hazard function + duration = time(1) + exprnd(time(3)); + duration = iff(duration > time(2), time(2), duration); + case 2 % A time sampled from a uniform distribution + duration = time(1) + (time(2) - time(1))*rand; + case 1 % A fixed time + duration = time(1); + otherwise % Pick on of the values + duration = randsample(time, 1); + end +end +end \ No newline at end of file diff --git a/tests/expDefinitions/advancedChoiceWorldExpPanel.m b/tests/expDefinitions/advancedChoiceWorldExpPanel.m new file mode 100644 index 00000000..929b1a28 --- /dev/null +++ b/tests/expDefinitions/advancedChoiceWorldExpPanel.m @@ -0,0 +1 @@ +% --pass \ No newline at end of file diff --git a/tests/expDefinitions/advancedChoiceWorld_parameters.mat b/tests/expDefinitions/advancedChoiceWorld_parameters.mat new file mode 100644 index 0000000000000000000000000000000000000000..d84446fbc143b407ec1e2f38af5d21fca954679c GIT binary patch literal 729 zcmeZu4DoSvQZUssQ1EpO(M`+DN!3vZ$Vn_o%P-2cQV4Jk_w+L}(NSB|pf2Qo1*RLt2LxvAI(x(06ymPa!w(wF!>CwsOo`3(e@B6;*@BVD+5wP0Z6yWsyG~eTCW9scEAA%T5w?1FM|DfDpVOSiWlF2^nmZ}_slc}{U%5V1P zOc7+{7Zndu_uMdZ?b-^d^H;XHwi(3U4)$yC(&1d}(NcAM-_dgk&IdjjmP$T<;HKO0 z`tGw^rn=&RqF$!QJsrt;`qFc~mkaJ&<^SpL z*O%2nEvsK_U%I3|^~bTIZ@YZ%-SvJ_yS3`hPve^CUFSFU=-o*_TrDX2eWy`tw|Y_J zzP&FV{qOm)F7iiJs#Jdc`Iq1K8q_WD{+)zyT`rR=?j_VO@VU{Yb0q uvD)`#6W$-!k=rW6zj(KohuD$Q67CP?#TARsD{P+F^Z&$qdGps#)&T&{M@$$1 literal 0 HcmV?d00001 diff --git a/tests/expDefinitions/choiceWorld.m b/tests/expDefinitions/choiceWorld.m new file mode 100644 index 00000000..bcf265e3 --- /dev/null +++ b/tests/expDefinitions/choiceWorld.m @@ -0,0 +1,636 @@ +function choiceWorld(t, events, p, visStim, inputs, outputs, audio) +% ChoiceWorld(t, events, parameters, visStim, inputs, outputs, audio) +% +% A simple training protocol closely following that of our manual training. +% Contrasts are presented randomly (no staircase). The session is ended +% automatically if after a minimum number of trials either the median +% response time over the last 20 trials is over 5x (default) longer than +% that of the whole session, or if there is a greater than 50% (default) +% decrease in performance over the last 20 trials (compared to total +% performance over the whole session). +% +% The wheel gain changes only after the subject completes over 200 trials +% in a session. The gain only changes once and remains changed for all +% future sessions. +% +% There is no longer any change in reward volume, and there is no cue +% interactive delay. + +%% Fixed parameters +contrastSet = p.contrastSet.at(events.expStart); +startingContrasts = p.startingContrasts.at(events.expStart); +repeatOnMiss = p.repeatOnMiss.at(events.expStart); +trialsToBuffer = p.trialsToBuffer.at(events.expStart); +trialsToZeroContrast = p.trialsToZeroContrast.at(events.expStart); +rewardSize = p.rewardSize.at(events.expStart); +initialGain = p.initialGain.at(events.expStart); +normalGain = p.normalGain.at(events.expStart); +responseWindow = p.responseWindow.at(events.expStart); + +% Sounds +audioDevice = audio.Devices('default'); +onsetToneFreq = 5000; +onsetToneDuration = 0.1; +onsetToneRampDuration = 0.01; +toneSamples = p.onsetToneAmplitude*events.expStart.map(@(x) ... + aud.pureTone(onsetToneFreq, onsetToneDuration, audioDevice.DefaultSampleRate, ... + onsetToneRampDuration, audioDevice.NrOutputChannels)); +missNoiseDuration = 0.5; +missNoiseSamples = p.missNoiseAmplitude*events.expStart.map(@(x) ... + randn(audioDevice.NrOutputChannels, audioDevice.DefaultSampleRate*missNoiseDuration)); + +%% Initialize trial data +trialDataInit = events.expStart.mapn(... + contrastSet, startingContrasts, repeatOnMiss, ... + trialsToBuffer, trialsToZeroContrast, rewardSize,... + @initializeTrialData).subscriptable; + +%% Set up wheel +wheel = inputs.wheelMM; +quiescThreshold = 1000; +% millimetersFactor = events.newTrial.map2(31*2*pi/(p.encoderRes*4), @times); % convert the wheel gain to a value in mm/deg +gain = events.expStart.mapn(initialGain, normalGain, @initWheelGain); +enoughTrials = events.trialNum > 200; +wheelGain = iff(enoughTrials, normalGain, gain); + +%% Trial event times +% (this is set up to be independent of trial conditon, that way the trial +% condition can be chosen in a performance-dependent manner) + +% Resetting pre-stim quiescent period +prestimQuiescentPeriod = at(p.prestimQuiescentTime.map(@(A)rnd.exp(A(3),1,A(1:2))), events.newTrial); +preStimQuiescence = sig.quiescenceWatch(prestimQuiescentPeriod, t, wheel, quiescThreshold); +% Stimulus onset +stimOn = at(true, preStimQuiescence); % FIXME test whether at is needed here +% Play tone at interactive onset +audio.default = toneSamples.at(stimOn); +% The wheel displacement is zeroed at stimOn +stimDisplacement = wheelGain*(wheel - wheel.at(stimOn)); + +responseTimeOver = (t - t.at(stimOn)) > responseWindow; % p.responseWindow may be set to Inf +threshold = stimOn.setTrigger(... + abs(stimDisplacement) >= abs(p.responseDisplacement) | responseTimeOver); +response = cond(... + responseTimeOver, 3,... % if the response time is over the response = 0 + true, -sign(stimDisplacement)); % otherwise it should be the inverse of the sign of the stimulusDisplacement +response = response.at(stimOn.setTrigger(threshold)); % only update the response signal when the threshold has been crossed + +%% Bias +bias = merge(response.keepWhen(response~=3).bufferUpTo(10).map(@sum), ... + at(0, events.expStart)); % Initialize with 0 at expStart + +%% Update performance at response +responseData = vertcat(stimDisplacement, events.trialNum, response, bias); +trialData = responseData.at(response).scan(@updateTrialData, trialDataInit).subscriptable; +% trialData = response.scan(@updateTrialData, trialDataInit, 'pars', stimDisplacement, events.trialNum, bias).subscriptable; +% Set trial contrast (chosen when updating performance) +trialContrast = trialData.trialContrast.at(events.newTrial); +hit = trialData.hit.at(response); + +%% Task disengagement +% Response time = duration (seconds) between new trial and response +rt = t.at(stimOn).map2(t, @(a,b)diff([a,b])).at(response); +% The median response time over the last 20 trials +windowedRT = rt.buffer(20).map(@median); +% The median response time over all trials +baselineRT = rt.bufferUpTo(1000).map(@median); +% tooSlow is true when windowed rt is x times longer than median rt for the +% session, where x is the rtCriterion +tooSlow = windowedRT > baselineRT*p.rtCriterion; +% noResponse is true when mouse fails to respond for over x seconds, where +% x is maxRespWindow +% noResponse = t-t.at(events.newTrial) > p.maxRespWindow; + +% A rolloing buffer of performance (proportion of last 20 trials that were +% correct) - this includes repeat on incorrect trials +windowedPerf = hit.buffer(20).map(@(a)sum(a)/length(a)); +% Proportion of all trials that were correct +baselinePerf = hit.bufferUpTo(1000).map(@(a)sum(a)/length(a)); +% True when there is an x% decrease in performance over the last 20 trials +% compared to the session average, where x is pctPerfDecrease +poorPerformance = iff(trialData.proportionLeft == 0.5, ... + (baselinePerf - windowedPerf)/baselinePerf > p.pctPerfDecrease/100, false); +% poorPerformance = (baselinePerf - windowedPerf)/baselinePerf > p.pctPerfDecrease/100; + +% The subject is identified as disengaged from the task when, after +% minTrials have been completed, the subject is either too slow or exhibits +% a significant drop in performance. If the subject has not completed the +% minimum number of trials in 45 minutes it is also classed as disengaged. +disengaged = iff(events.trialNum > p.minTrials, tooSlow, ... + events.expStart.delay(60*45)); +% The session is finished when either the session has been running for x +% seconds, where x is trialDataInit.endAfter (20min on the first day, 40min +% on the seconds, Inf otherwise), or when the subject is disengaged +% finish = merge(at(true, disengaged),... +% at(true, events.expStart.delay(trialDataInit.endAfter))); +finish = cond(disengaged, true,... + events.expStart.delay(trialDataInit.endAfter), true); + +% When finish takes a value (it may only sample true), this is posted to +% events.expStop to trigger the end of the session +expStop = events.expStop; +expStop.Node.Listeners = [expStop.Node.Listeners, ... + into(finish, expStop)]; + +%% Give feedback and end trial +% Ensures reward size is not re-calculated at the response time +rewardSize = trialData.rewardSize.at(events.newTrial); +% NOTE: there is a 10ms delay for water output, because otherwise water and +% stim output compete and stim is delayed +outputs.reward = rewardSize.at(hit==true).delay(0.01); +% Play noise on miss +audio.default = missNoiseSamples.at(delay(hit==false, 0.01)); +% ITI defined by outcome +iti = iff(hit==1, p.itiHit, p.itiMiss); +% Stim stays on until the end of the ITI +stimOff = threshold.delay(iti); + +%% Visual stimulus +% Azimuth control +% 1) stim fixed in place until interactive on +% 2) wheel-conditional during interactive +% 3) fixed at response displacement azimuth after response +trialSide = trialData.trialSide.at(stimOn); +azimuth = cond( ... + stimOn.to(threshold), p.startingAzimuth*trialSide + stimDisplacement, ... + threshold.to(events.newTrial), ... + p.startingAzimuth*trialSide + ... + iff(response~=3, -response*abs(p.responseDisplacement), trialSide*abs(p.responseDisplacement))); + +% Stim flicker +% stimFlicker = sin((t - t.at(stimOn))*stimFlickerFrequency*2*pi) > 0; +stim = vis.grating(t, 'sine', 'gaussian'); +stim.sigma = p.sigma; +stim.spatialFreq = p.spatialFreq; +stim.phase = 2*pi*events.newTrial.map(@(v)rand); +stim.azimuth = azimuth; +%stim.contrast = trialContrast.at(stimOn)*stimFlicker; +stim.contrast = trialContrast; +stim.show = stimOn.to(stimOff); + +visStim.stim = stim; + +%% Display and save +% events.pPerf = (baselinePerf - windowedPerf)/baselinePerf > p.pctPerfDecrease/100; +% Wheel and stim +events.azimuth = azimuth; + +% Trial times +events.prestimQuiescentPeriod = prestimQuiescentPeriod; +events.stimulusOn = stimOn; +events.interactiveOn = stimOn; +events.stimulusOff = stimOff; +events.feedback = iff(hit==1, hit, -1); +events.threshold = threshold; +% End trial samples a false when the next trial is to be a repeat trial. +% NB: the identity function is used to ensure that stimOff takes a value +% before endTrial +events.endTrial = at(~trialData.repeatTrial, stimOff.identity); +% Used to identify what form of disengagement has occured +events.disengaged = skipRepeats(keepWhen(cond(... + tooSlow, 'long RT',... + true, 'false'), events.trialNum > p.minTrials)); +events.windowedRT = windowedRT.map(fun.partial(@sprintf, '%.1f sec')); +events.baselineRT = baselineRT.map(fun.partial(@sprintf, '%.1f sec')); +events.pctDecrease = map(((baselinePerf - windowedPerf)/baselinePerf)*100, fun.partial(@sprintf, '%.1f%%')); +events.endAfter = trialDataInit.endAfter/60; + +% Trial side probability +events.bias = bias; + +% Performance +events.contrastSet = trialData.contrastSet; +events.repeatOnMiss = trialData.repeatOnMiss; +events.contrastLeft = iff(trialData.trialSide == -1, trialData.trialContrast, trialData.trialContrast*0); +events.contrastRight = iff(trialData.trialSide == 1, trialData.trialContrast, trialData.trialContrast*0); +% events.trialSide = trialData.trialSide; +events.hit = hit; +events.response = at(iff(response==3, 0, response), threshold); +events.useContrasts = trialData.useContrasts; +events.trialsToZeroContrast = trialData.trialsToZeroContrast; +events.hitBuffer = trialData.hitBuffer; +events.wheelGain = wheelGain; +events.totalWater = outputs.reward.scan(@plus, 0).map(fun.partial(@sprintf, '%.1fl')); + +%% Defaults +try +% The entire stimulus/target contrast set +p.contrastSet = [1,0.5,0.25,0.125,0.06,0]'; +% (which conrasts to use at the beginning of training) +p.startingContrasts = double([true,true,false,false,false,false]'); +% (which contrasts to repeat on incorrect) +p.repeatOnMiss = double([true,true,false,false,false,false]'); +% (number of trials to judge rolling performance) +p.trialsToBuffer = 50; +% (number of trials after introducing 12.5% contrast to introduce 0%) +p.trialsToZeroContrast = 200; +p.spatialFreq = 1/10; +p.sigma = [7, 7]'; +% stimFlickerFrequency = 5; % DISABLED BELOW +p.startingAzimuth = 35; % (degrees) +p.responseDisplacement = 35; % (degrees) +% Starting reward size (this value is ignored after the first session) +p.rewardSize = 3; % (microliters) +% Initial wheel gain +p.initialGain = 8; % ~= 20 @ 90 deg; +p.normalGain = 4; % ~= 10 @ 90 deg; + +% Timing +p.prestimQuiescentTime = [0.2, 0.5, 0.35]'; % (seconds) +% p.cueInteractiveDelay = 0.2; +% Inter-trial interval on correct response +p.itiHit = 1; % (seconds) +% Inter-trial interval on incorrect response +p.itiMiss = 2; % (seconds) +p.responseWindow = 60; % (seconds) + +% How many times slower the subject must become in order to be marked as +% disengaged +p.rtCriterion = 5; % (multiplier) +% The percent decrease in performance that subject must exhibit to be +% marked as disengaged +p.pctPerfDecrease = 50; % (percent) +% The minimum number of trials to be completed before the subject may be +% classified as disengaged +p.minTrials = 400; +% The maximum number of seconds the subject can take to give a response +% before being classified as disengaged +p.maxRespWindow = 60; % (seconds) + +% Audio +p.missNoiseAmplitude = 0.01; +p.onsetToneAmplitude = 0.15; +catch +end +end +function wheelGain = initWheelGain(expRef, initialGain, normalGain) +subject = dat.parseExpRef(expRef); +expRef = dat.listExps(subject); +wheelGain = initialGain; +if length(expRef) > 1 + % Loop through blocks from latest to oldest, if any have the relevant + % parameters then carry them over + for check_expt = length(expRef)-1:-1:1 + previousBlockFilename = dat.expFilePath(expRef{check_expt}, 'block', 'master'); + trialNum = []; + if exist(previousBlockFilename,'file') + previousBlock = load(previousBlockFilename); + if isfield(previousBlock.block,'events')&&isfield(previousBlock.block.events,'newTrialValues') + trialNum = previousBlock.block.events.newTrialValues; + end + end + % Check if the relevant fields exist + if length(trialNum) > 200 + % Break the loop and use these parameters + wheelGain = normalGain; + break + end + end +end +end + +function trialDataInit = initializeTrialData(expRef, ... + contrastSet,startingContrasts,repeatOnMiss,trialsToBuffer, ... + trialsToZeroContrast,rewardSize) + +%%%% Get the subject +% (from events.expStart - derive subject from expRef) +subject = dat.parseExpRef(expRef); + +startingContrasts = logical(startingContrasts)'; +repeatOnMiss = logical(repeatOnMiss)'; + +%%%% Initialize all of the session-independent performance values +trialDataInit = struct; + +% Store which trials are repeated on miss +trialDataInit.repeatOnMiss = repeatOnMiss; +% Set up the flag for repeating incorrect +trialDataInit.repeatTrial = false; +% Initialize hit/miss +trialDataInit.hit = nan; + +%%%% Load the last experiment for the subject if it exists +% (note: MC creates folder on initilization, so start search at 1-back) +expRef = dat.listExps(subject); +% Check how many days mouse has been trained +[~, dates] = dat.parseExpRef(expRef); +dayNum = find(floor(now) == unique(dates), 1, 'last'); +trialDataInit.endAfter = iff(dayNum<3, 60*20*dayNum, Inf); +trialDataInit.endAfter = Inf; + +useOldParams = false; +if length(expRef) > 1 + % Loop through blocks from latest to oldest, if any have the relevant + % parameters then carry them over + for check_expt = length(expRef)-1:-1:1 + learned = isLearned(expRef{check_expt}); + previousBlockFilename = dat.expFilePath(expRef{check_expt}, 'block', 'master'); + if exist(previousBlockFilename,'file') + previousBlock = load(previousBlockFilename); + if ~isfield(previousBlock.block, 'outputs')||... + ~isfield(previousBlock.block.outputs, 'rewardValues')||... + isempty(previousBlock.block.outputs.rewardValues) + lastRewardSize = rewardSize; + else + lastRewardSize = previousBlock.block.outputs.rewardValues(end); + end + + if isfield(previousBlock.block,'events') + previousBlock = previousBlock.block.events; + else + previousBlock = []; + end + end + % Check if the relevant fields exist + if exist('previousBlock','var') && all(isfield(previousBlock, ... + {'useContrastsValues','hitBufferValues','trialsToZeroContrastValues'})) &&... + length(previousBlock.newTrialValues) > 5 + % Break the loop and use these parameters + useOldParams = true; + break + end + end +end + +if useOldParams + % If the last experiment file has the relevant fields, set up performance + + % Which contrasts are currently in use + try + len = length(previousBlock.contrastSetValues)/length(previousBlock.contrastSetTimes); + trialDataInit.contrastSet = previousBlock.contrastSetValues(end-len+1:end); + catch + len = length(contrastSet'); + trialDataInit.contrastSet = contrastSet'; + end + trialDataInit.useContrasts = previousBlock.useContrastsValues(end-len+1:end); + + % The buffer to judge recent performance for adding contrasts + trialDataInit.hitBuffer = ... + previousBlock.hitBufferValues(:,end-len+1:end,:); + + % The countdown to adding 0% contrast + trialDataInit.trialsToZeroContrast = previousBlock.trialsToZeroContrastValues(end); + + % If zero contrasts have been introduced and lapse rate is < 0.2 for + % 50% contrasts, remove them. +% if trialDataInit.trialsToZeroContrast == 0 && ... +% sum(trialDataInit.hitBuffer(:,2,1))/size(trialDataInit.hitBuffer,1) > 0.8 && ... +% sum(trialDataInit.hitBuffer(:,2,2))/size(trialDataInit.hitBuffer,1) > 0.8 +% trialDataInit.useContrasts(trialDataInit.contrastSet == 0.5) = false; +% end + + % If the subject did over 200 trials last session, reduce the reward by + % 0.1, unless it is 2ml + if length(previousBlock.newTrialValues) > 200 && lastRewardSize > 1.5 + trialDataInit.rewardSize = lastRewardSize-0.1; + else + trialDataInit.rewardSize = lastRewardSize; + end + if learned + % Remove repeat on incorrect + trialDataInit.repeatOnMiss = zeros(1,length(trialDataInit.contrastSet)); + end + +else + % If this animal has no previous experiments, initialize performance + % Store the contrasts which are used + trialDataInit.contrastSet = contrastSet'; + trialDataInit.useContrasts = startingContrasts; + trialDataInit.hitBuffer = nan(trialsToBuffer, length(contrastSet), 2); % two tables, one for each side + trialDataInit.trialsToZeroContrast = trialsToZeroContrast; + % Initialize water reward size & wheel gain + trialDataInit.rewardSize = rewardSize; +end + +% Set the first contrast +contrasts = trialDataInit.contrastSet(trialDataInit.useContrasts); +w = ((contrasts~=0) + 1) / length(unique([contrasts, -contrasts])); +trialDataInit.trialContrast = randsample(contrasts, 1, true, w); +trialDataInit.trialSide = iff(rand <= 0.5, -1, 1); +end + +function trialData = updateTrialData(trialData,responseData) +% Update the performance and pick the next contrast +stimDisplacement = responseData(1); +response = responseData(3); +% bias normalized by trial number: abs(bias) = 0:1 +bias = responseData(4)/10; +% windowedRT = responseData(2); +% trialNum = responseData(3); + +% if trialNum > 50 && windowedRT < 60 +% trialData.wheelGain = 3; +% end +% +%%%% Get index of current trial contrast +currentContrastIdx = trialData.trialContrast == trialData.contrastSet; + +%%%% Define response type based on trial condition +trialData.hit = response~=3 && stimDisplacement*trialData.trialSide < 0; + +% Index for whether contrast was on the left or the right as performance is +% calculated for both sides. If the contrast was on the left, the index is +% 1, otherwise 2 +trialSideIdx = iff(trialData.trialSide<0, 1, 2); + + +%%%% Update buffers and counters if not a repeat trial +if ~trialData.repeatTrial + %%%% Contrast-adding performance buffer + % Update hit buffer for running performance + trialData.hitBuffer(:,currentContrastIdx,trialSideIdx) = ... + [trialData.hit;trialData.hitBuffer(1:end-1,currentContrastIdx,trialSideIdx)]; +end + +%%%% Add new contrasts as necessary given performance +% This is based on the last trialsToBuffer trials for rolling performance +% (these parameters are hard-coded because too specific) +% (these are side-dependent) +current_min_contrast = min(trialData.contrastSet(trialData.useContrasts & trialData.contrastSet ~= 0)); +trialsToBuffer = size(trialData.hitBuffer,1); +switch current_min_contrast + + case 0.5 + % Lower from 0.5 contrast after > 70% correct + min_hit_percentage = 0.70; + + contrast_buffer_idx = ismember(trialData.contrastSet,[0.5,1]); + contrast_total_trials = sum(~isnan(trialData.hitBuffer(:,contrast_buffer_idx,:))); + % If there have been enough buffer trials, check performance + if sum(contrast_total_trials) >= size(trialData.hitBuffer,1) + % Sample as evenly as possible across pooled contrasts. Here + % we pool the columns representing the 50% and 100% contrasts + % for each side (dim 3) individually, then shift the dimentions + % so that pooled_hits(1,:) = all 50% and 100% trials on the + % left, and pooled_hits(2,:) = all 50% and 100% trials on the + % right. + pooled_hits = shiftdim(... + reshape(trialData.hitBuffer(:,contrast_buffer_idx,:),[],1,2), 2); + use_hits(1) = sum(pooled_hits(1,(find(~isnan(pooled_hits(1,:)),trialsToBuffer/2)))); + use_hits(2) = sum(pooled_hits(2,(find(~isnan(pooled_hits(2,:)),trialsToBuffer/2)))); + min_hits = find(1 - binocdf(1:trialsToBuffer/2,trialsToBuffer/2,min_hit_percentage) < 0.05,1); + if all(use_hits >= min_hits) + trialData.useContrasts(find(~trialData.useContrasts,1)) = true; + end + end + + case 0.25 + % Lower from 0.25 contrast after > 50% correct + min_hit_percentage = 0.70; + + contrast_buffer_idx = ismember(trialData.contrastSet,current_min_contrast); + contrast_total_trials = sum(~isnan(trialData.hitBuffer(:,contrast_buffer_idx,:))); + % If there have been enough buffer trials, check performance + if sum(contrast_total_trials) >= size(trialData.hitBuffer,1) + % Sample as evenly as possible across pooled contrasts + pooled_hits = shiftdim(... + reshape(trialData.hitBuffer(:,contrast_buffer_idx,:),[],1,2), 2); + use_hits(1) = sum(pooled_hits(1,(find(~isnan(pooled_hits(1,:)),trialsToBuffer/2)))); + use_hits(2) = sum(pooled_hits(2,(find(~isnan(pooled_hits(2,:)),trialsToBuffer/2)))); + min_hits = find(1 - binocdf(1:trialsToBuffer/2,trialsToBuffer/2,min_hit_percentage) < 0.05,1); + if all(use_hits >= min_hits) + trialData.useContrasts(find(~trialData.useContrasts,1)) = true; + end + end + + case 0.125 + % Lower from 0.125 contrast after > 65% correct + min_hit_percentage = 0.65; + + contrast_buffer_idx = ismember(trialData.contrastSet,current_min_contrast); + contrast_total_trials = sum(~isnan(trialData.hitBuffer(:,contrast_buffer_idx,:))); + % If there have been enough buffer trials, check performance + if sum(contrast_total_trials) >= size(trialData.hitBuffer,1) + % Sample as evenly as possible across pooled contrasts + pooled_hits = shiftdim(... + reshape(trialData.hitBuffer(:,contrast_buffer_idx,:),[],1,2), 2); + use_hits(1) = sum(pooled_hits(1,(find(~isnan(pooled_hits(1,:)),trialsToBuffer/2)))); + use_hits(2) = sum(pooled_hits(2,(find(~isnan(pooled_hits(2,:)),trialsToBuffer/2)))); + min_hits = find(1 - binocdf(1:trialsToBuffer/2,trialsToBuffer/2,min_hit_percentage) < 0.05,1); + if all(use_hits >= min_hits) + trialData.useContrasts(find(~trialData.useContrasts,1)) = true; + end + end + +end + +% 200 trials after 12.5 % contrast introduced, put 6% +% 400 trials after 12.5 % contrast introduced, put 0% +% 600 trials after 12.5 % contrast introduced, remove 50% +if min(trialData.contrastSet(trialData.useContrasts)) <= 0.125 && ... + trialData.trialsToZeroContrast > 0 + % Subtract one from the countdown + trialData.trialsToZeroContrast = trialData.trialsToZeroContrast-1; + + if trialData.trialsToZeroContrast == 0 && ... + ~trialData.useContrasts(trialData.contrastSet == 0.06) + trialData.useContrasts(trialData.contrastSet == 0.06) = true; % Add 6% + trialData.trialsToZeroContrast = 200; % Reset counter + + elseif trialData.trialsToZeroContrast == 0 && ... + ~trialData.useContrasts(trialData.contrastSet == 0) + trialData.useContrasts(trialData.contrastSet == 0) = true; % Add 0% + trialData.trialsToZeroContrast = 200; % Reset counter + + elseif trialData.trialsToZeroContrast == 0 && ... + trialData.useContrasts(trialData.contrastSet == 0) + trialData.useContrasts(trialData.contrastSet == 0.5) = false; % Remove 50% + end +end + +%%%% Set flag to repeat - skip trial choice if so +if ~trialData.hit && any(trialData.repeatOnMiss==true) && ... + ismember(trialData.trialContrast,trialData.contrastSet(trialData.repeatOnMiss)) + % If the response is a no-go, repeat the same trial side + if response ~= 3 + % Otherwise take biased sample from normal distribution + sd = 0.5; % standard deviation + r = 0.5 + sd.*randn; % pull number from normal dist with mean 0.5 + trialData.trialSide = iff((r - bias) > 0.5, 1, -1); + % trialData.trialSide = iff(binornd(1,bias), + end + trialData.repeatTrial = true; + return +else + trialData.repeatTrial = false; +end + +%%%% Pick next contrast + +% Next contrast is random from current contrast set +contrasts = trialData.contrastSet(trialData.useContrasts); +w = ((contrasts~=0) + 1) / length(unique([contrasts, -contrasts])); +trialData.trialContrast = randsample(contrasts, 1, true, w); +%%%% Pick next side +trialData.trialSide = iff(rand <= 0.5, -1, 1); +end +function learned = isLearned(ref) +learned = false; +subject = dat.parseExpRef(ref); +expRef = dat.listExps(subject); +j = 1; +pooledCont = []; +pooledIncl = []; +pooledChoice = []; +for i = length(expRef):-1:1 + p = dat.expFilePath(expRef{i}, 'block', 'master'); + if exist(p,'file')==2 + % Block doesn't exist + p = fileparts(p); + else + fprintf('No block file for session %s: skipping\n', expRef{i}) + continue + end + try + feedback = readNPY(fullfile(p,'_ibl_trials.feedbackType.npy')); + contrastLeft = readNPY(fullfile(p,'_ibl_trials.contrastLeft.npy')); + contrastRight = readNPY(fullfile(p,'_ibl_trials.contrastRight.npy')); + incl = readNPY(fullfile(p,'_ibl_trials.included.npy')); + choice = readNPY(fullfile(p,'_ibl_trials.choice.npy')); + catch + warning('isLearned:ALFLoad:MissingFiles', ... + 'Unable to load files for session %s', expRef{i}) + continue + end + % If the zero contrast stimuli have not been introduced, the subject + % can't have learned. NB: Unfortunately if the hand of fate not once + % chose a zero contrast trial then the mouse would fail here, even if it + % was available to sample. This is fairly unlikely to happen and this + % method is much quicker than loading the block file to retreive the + % actual contrast set. + contrast = diff([contrastLeft,contrastRight],[],2); + if ~any(contrast==0) + fprintf('Low contrasts not yet introduced\n') + return + end + perfOnEasy = sum(feedback==1 & abs(contrast > 0.25)) / sum(abs(contrast > 0.25)); + if length(feedback) > 200 && perfOnEasy > 0.8 + pooledCont = [pooledCont; contrast]; + pooledIncl = [pooledIncl; incl]; + pooledChoice = [pooledChoice; choice]; + if j < 3 + j = j+1; + else + % All three sessions meet criteria + contrastSet = unique(pooledCont); + nn = arrayfun(@(c)sum(pooledCont==c & pooledIncl), contrastSet); + pp = arrayfun(@(c)sum(pooledCont==c & pooledIncl & pooledChoice==-1), contrastSet)./nn; + pars = psy.mle_fit_psycho([contrastSet';nn';pp'], 'erf_psycho_2gammas',... + [mean(contrastSet), 3, 0.05, 0.05],... + [min(contrastSet), 10, 0, 0],... + [max(contrastSet), 30, 0.4, 0.4]); + if abs(pars(1)) < 16 && pars(2) < 19 && pars(3) < 0.2 && pars(4) < 0.2 + learned = true; + else + fprintf('Fit parameter values below threshold\n') + return + end + end + else + fprintf('Low trial count or performance at high contrast\n') + return + end +end +end \ No newline at end of file diff --git a/tests/expDefinitions/choiceWorld_parameters.mat b/tests/expDefinitions/choiceWorld_parameters.mat new file mode 100644 index 0000000000000000000000000000000000000000..35d67dbd3e5e9d754c33dd1387b8e9ef492ba80f GIT binary patch literal 709 zcmeZu4DoSvQZUssQ1EpO(M`+DN!3vZ$Vn_o%P-2cQV4Jk_w+L}(NSB|pf+cGgQRLogBb$`CFp~&&t zpBLplLU=-wk`moTbvP7VRRbrt%KJ8JEtD?Me?Lk6=(oiE!Ojth@44z-o^Mjzc;MI( z9%&)&dZvA+pB9z(a&4`+WPLjR_v3W?{gby#B&$pJti5^7Y|XJ}*UtQkJGefgf5$T> zjxCXAvNycV`Ln8(S=F=HncYFh+&M=0RG_uAPb9BkL6wsHLc^avmrkgsEs#5w$gxYw zj(x3|cf_9=uOBj=OOR||=(4GJx6IR^DPG;H(h7w&Gam>%4UkPwQo7*ac|-Ifi>91f zHuvuR$GLnxdvyFYz9q8=H!NY~I&uBYf=)FXhD7!m%eJ}A)(>g@5a{6V*e=UqQZRY0 z!vdKl>bg0TZ+`wVA>!tmh*MFTCwCi*81@kRH>P0{v->Q~g}lFM8zqWLxTA*63^3qn4R-|8=cZ-IiIazPx_>v$x;Z{GGK` zK04j}{=ZxH*14jO@9cJ&bIa-TKl%N|jPK_@wk$Oe_ttj8Xs@}_TP-Xo(Wh1Ux66FKUh*X7)yBk= z!sm78J>PMDmhyM~pKInm&dRC1yI1b{*AE9@{uBRyF!lZ8^xAX2+%d_l*R>0-Z>syf z@Bf(_*KPm)k`+C2QakOB-X#mWProg!f`aG2d7WM}!%ddsviWtp>N Date: Wed, 20 Feb 2019 16:00:33 +0000 Subject: [PATCH 028/108] modify alyx_matlab --- alyx-matlab | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alyx-matlab b/alyx-matlab index c5c5c1ba..a013557f 160000 --- a/alyx-matlab +++ b/alyx-matlab @@ -1 +1 @@ -Subproject commit c5c5c1ba22dce86549ed56fd257c3852f5949391 +Subproject commit a013557feab80af3b90903a6b4cd06b8b5c85293 From fde8177a6a44164d8365b95b201c08955b5c4079 Mon Sep 17 00:00:00 2001 From: kevin-j-miller Date: Wed, 20 Feb 2019 16:01:37 +0000 Subject: [PATCH 029/108] move alyxpanel property to private setaccess --- +eui/MControl.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/+eui/MControl.m b/+eui/MControl.m index 9d5c201c..0c432533 100644 --- a/+eui/MControl.m +++ b/+eui/MControl.m @@ -17,10 +17,10 @@ properties LoggingDisplay % control for showing log output - AlyxPanel % holds the AlyxPanel object (see buildUI(), eui.AlyxPanel()) end properties (SetAccess = private) + AlyxPanel % holds the AlyxPanel object (see buildUI(), eui.AlyxPanel()) LogSubject % Subject selector control NewExpSubject % Experiment selector control NewExpType % Experiment type selector control From 6f07fc8ec517b60ec593733f3e57daea43221795 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Thu, 21 Feb 2019 18:32:30 +0200 Subject: [PATCH 030/108] Bugfix for registering hw info --- +srv/expServer.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/+srv/expServer.m b/+srv/expServer.m index 7fb3ca2f..7af7d14c 100644 --- a/+srv/expServer.m +++ b/+srv/expServer.m @@ -274,7 +274,8 @@ function handleMessage(id, data, host) rig.stimWindow.flip(); % clear the screen after % save a copy of the hardware in JSON - fid = fopen(dat.expFilePath(expRef, 'hw-info', 'master', 'json'), 'w'); + hwInfo = dat.expFilePath(expRef, 'hw-info', 'master', 'json'); + fid = fopen(hwInfo, 'w'); fprintf(fid, '%s', obj2json(rig)); fclose(fid); if ~strcmp(dat.parseExpRef(expRef), 'default') From 9b5cb2a1840fb08e78e4a13a2052d8d495b2276c Mon Sep 17 00:00:00 2001 From: jaib1 Date: Fri, 22 Feb 2019 16:42:02 +0000 Subject: [PATCH 031/108] added test for 'vis.sinusoidLayer' and updated 'alyx' (dev) and 'signals' (dev) submodules --- alyx-matlab | 2 +- signals | 2 +- tests/sinusoidLayer_test.m | 49 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 tests/sinusoidLayer_test.m diff --git a/alyx-matlab b/alyx-matlab index a013557f..b44b998d 160000 --- a/alyx-matlab +++ b/alyx-matlab @@ -1 +1 @@ -Subproject commit a013557feab80af3b90903a6b4cd06b8b5c85293 +Subproject commit b44b998da6323289adeaf404eddd637251174435 diff --git a/signals b/signals index c81b99e0..4b45e84b 160000 --- a/signals +++ b/signals @@ -1 +1 @@ -Subproject commit c81b99e0922dbab9ab5d9a9770ff476c96fb6126 +Subproject commit 4b45e84b15b052bbba897126b9b6a27e4c7cd0e6 diff --git a/tests/sinusoidLayer_test.m b/tests/sinusoidLayer_test.m new file mode 100644 index 00000000..41fe9001 --- /dev/null +++ b/tests/sinusoidLayer_test.m @@ -0,0 +1,49 @@ +%% Test 1: vis.grating default values +azimuth = 0; spatialFreq = 1/15; phase = 0; orientation = 0; +[layer, image] = vis.sinusoidLayer(azimuth, spatialFreq, phase, orientation); +TestAns = [layer.texOffset(1), layer.texAngle, layer.size(1)]; +ExpectedAns = [0 0 15]; +assert(isequal(TestAns,ExpectedAns), 'Test 1 failed.'); + +%% Test 2: Negative Azimuth +azimuth = -90; spatialFreq = 1/15; phase = 0; orientation = 0; +[layer, image] = vis.sinusoidLayer(azimuth, spatialFreq, phase, orientation); +TestAns = [layer.texOffset(1), layer.texAngle, layer.size(1)]; +ExpectedAns = [-90 0 15]; +assert(isequal(TestAns,ExpectedAns), 'Test 2 failed.'); + +%% Test 3: High Spatial Frequency +azimuth = 0; spatialFreq = 2; phase = 0; orientation = 0; +[layer, image] = vis.sinusoidLayer(azimuth, spatialFreq, phase, orientation); +TestAns = round([layer.texOffset(1), layer.texAngle, layer.size(1)],4); +ExpectedAns = [0 0 0.5000]; +assert(isequal(TestAns,ExpectedAns), 'Test 3 failed.'); + + +%% Test 4: Negative Phase +azimuth = 0; spatialFreq = 1/15; phase = -90; orientation = 0; +[layer, image] = vis.sinusoidLayer(azimuth, spatialFreq, phase, orientation); +TestAns = round([layer.texOffset(1), layer.texAngle, layer.size(1)],4); +ExpectedAns = [10.1408 0 15.0000]; +assert(isequal(TestAns,ExpectedAns), 'Test 4 failed.'); + +%% Test 5: Negative Orientation +azimuth = 0; spatialFreq = 1/15; phase = 0; orientation = -90; +[layer, image] = vis.sinusoidLayer(azimuth, spatialFreq, phase, orientation); +TestAns = [layer.texOffset(1), layer.texAngle, layer.size(1)]; +ExpectedAns = [0 -90 15]; +assert(isequal(TestAns,ExpectedAns), 'Test 5 failed.'); + +%% Test 6: Non-zero values for all input args +azimuth = 45; spatialFreq = 7/15; phase = 30; orientation = 60; +[layer, image] = vis.sinusoidLayer(azimuth, spatialFreq, phase, orientation); +TestAns = round([layer.texOffset(1), layer.texAngle, layer.size(1)],4); +ExpectedAns = [24.1600 60.0000 2.1429]; +assert(isequal(TestAns,ExpectedAns), 'Test 6 failed.'); + +%% Test 7: Impossible Spatial Frequency +azimuth = 45; spatialFreq = -1/15; phase = 30; orientation = 60; +[layer, image] = vis.sinusoidLayer(azimuth, spatialFreq, phase, orientation); +TestAns = round([layer.texOffset(1), layer.texAngle, layer.size(1)],4); +ExpectedAns = [10.8803 60.0000 -15.0000]; +assert(isequal(TestAns,ExpectedAns), 'Test 7 failed.'); \ No newline at end of file From aa123dbe2618e48bfac39815f7c8e38f3a48f8a1 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Tue, 26 Feb 2019 15:52:17 +0200 Subject: [PATCH 032/108] Started AlyxPanel tests --- +eui/AlyxPanel.m | 4 ++-- alyx-matlab | 2 +- tests/+dat/paths.m | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/+eui/AlyxPanel.m b/+eui/AlyxPanel.m index f8b6da14..1e367cf3 100644 --- a/+eui/AlyxPanel.m +++ b/+eui/AlyxPanel.m @@ -211,7 +211,7 @@ function delete(obj) end end - function login(obj) + function login(obj, varargin) % Used both to log in and out of Alyx. Logging means to % generate an Alyx token with which to send/request data. % Logging out does not cause the token to expire, instead the @@ -222,7 +222,7 @@ function login(obj) % Are we logging in or out? if ~obj.AlyxInstance.IsLoggedIn % logging in % attempt login - obj.AlyxInstance = obj.AlyxInstance.login(); % returns an instance if success, empty if you cancel + obj.AlyxInstance = obj.AlyxInstance.login(varargin{:}); % returns an instance if success, empty if you cancel if obj.AlyxInstance.IsLoggedIn % successful % Start log in timer, to automatically log out after 30 % minutes of 'inactivity' (defined as not calling diff --git a/alyx-matlab b/alyx-matlab index dd2ab36d..8b4f4f96 160000 --- a/alyx-matlab +++ b/alyx-matlab @@ -1 +1 @@ -Subproject commit dd2ab36de59843edc94c15956967731d81173b74 +Subproject commit 8b4f4f96f7cfe4094cb4b2b8690aeb454f22c7b7 diff --git a/tests/+dat/paths.m b/tests/+dat/paths.m index 95e39049..7d22e839 100644 --- a/tests/+dat/paths.m +++ b/tests/+dat/paths.m @@ -18,9 +18,8 @@ p.rigbox = fileparts(which('addRigboxPaths')); % Repository for local copy of everything generated on this rig p.localRepository = 'C:\LocalExpData'; -p.localAlyxQueue = fullfile(p.rigbox, 'tests', 'data', 'alyx'); -p.databaseURL = 'https://alyx-dev.cortexlab.net'; -% p.databaseURL = 'https://dev.alyx.internationalbrainlab.org/'; +p.localAlyxQueue = fullfile(p.rigbox, 'alyx-matlab', 'tests', 'data'); +p.databaseURL = 'https://test.alyx.internationalbrainlab.org'; p.gitExe = 'C:\Program Files\Git\cmd\git.exe'; % Under the new system of having data grouped by mouse From cf9a2c2a4d940a443e46fa5ebdba62840d7f3923 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Wed, 27 Feb 2019 13:06:13 +0200 Subject: [PATCH 033/108] Change to validation params --- tests/expDefinitions/choiceWorld_parameters.mat | Bin 709 -> 701 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/expDefinitions/choiceWorld_parameters.mat b/tests/expDefinitions/choiceWorld_parameters.mat index 35d67dbd3e5e9d754c33dd1387b8e9ef492ba80f..0a70ff65f120214e81f5776ea788184a1a589793 100644 GIT binary patch delta 614 zcmV-s0-62A1-%83G#FQ9WFSUmVjwa%ATcvKFflqbHXt%EF*%V@BavVQk#rD$H39$t zc$~eJzi$&U6vv&*kG2dI14x}%*ils$gpfi>)MVjD+n`G52HeZL1`GW6=3V7g2KA*pNpL@^GFY+cK{*)5P6NVVh<=CcpZqWA(PPF8e664Kucv^!IKtf41{SJHPFoU3u|s|LnBu#@Rk6 zj@z%91?FwtlWTZ#fd|)rGStgP!QJ==Zo}%&E}ag(ES`6L@@*0SxYu)g=M{Iy#vPu+ zHCM#lGUN5D%swppCeH0s_fPw%_hC?KR`A)!RZs4gC)f1k+JC_Xzi|OsTl4Ra;+)=i z^+#FP|9zf2p4@v6F8kkqZ5G_z;aV%Wp~GFY>)y@qp)$8|{tWv)B1*HXNQDE`)ATqz zz{#%jI%+jS97R(mTsv`{QRnzmvBZV<#Jot=ZN#tj6Zn%=%uz zuUL<`<^l+`S5Gh44{-KuSOeJ7`Xx908lto@p`A6sWP z-~QS^J8c!?+`B}a+CuYx{vW?{0q>pTJ$A>D`|QZo9k~rhuHnc%V1K`lDz};GcT@Y+ za^+uNKkfdQy-E4r1$=!4f5ZCwcZq$;>o!d7TgxR6w;c1=bmUy>v0=#*&n=$kxX+p+ z_b>UZo#Se3ecint=k@Tx>HZi0!Da8`XMN613tVFb*DG+>vVQwTzW>bF*FF9E9TmjM zD`xyI(l|cy`#3sMQ=jbXXMC7O9SGAm*FJlGFjg4Aq0*D#ZBfnFs`zV__(?yA4JpCc IAFB?~ekw&PWB>pF From 6d78c2c820216e06674792ccb7e4377c5e2cd842 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Thu, 28 Feb 2019 16:49:52 +0200 Subject: [PATCH 034/108] Fix for xlim when single water restricted weight --- +eui/AlyxPanel.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/+eui/AlyxPanel.m b/+eui/AlyxPanel.m index 1e367cf3..53d573fc 100644 --- a/+eui/AlyxPanel.m +++ b/+eui/AlyxPanel.m @@ -574,7 +574,7 @@ function viewSubjectHistory(obj, ax) box(ax, 'off'); % Change the plot x axis limits maxDate = max(dates([records.is_water_restricted]|~isnan([records.weighing_at]))); - if numel(dates) > 1 && ~isempty(maxDate) + if numel(dates) > 1 && ~isempty(maxDate) && min(dates) ~= maxDate xlim(ax, [min(dates) maxDate]) else maxDate = now; From 4f0cf69179993cddadf5160d19763f083cb67423 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Tue, 5 Mar 2019 14:08:38 +0200 Subject: [PATCH 035/108] Bug fix for differing def fun paths --- tests/inferParamsTest.m | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/inferParamsTest.m b/tests/inferParamsTest.m index a6e0a1bc..1b34cb19 100644 --- a/tests/inferParamsTest.m +++ b/tests/inferParamsTest.m @@ -12,6 +12,12 @@ pars = exp.inferParameters([expDefPath filesep 'advancedChoiceWorld.m']); load(fullfile(expDefPath, 'advancedChoiceWorld_parameters.mat')); +assert(strcmp(pars.defFunction, [expDefPath filesep 'advancedChoiceWorld.m']), ... + 'Incorrect expDef path') + +% Remove defFunction field before comparison +pars = rmfield(pars, 'defFunction'); +parameters = rmfield(parameters, 'defFunction'); assert(isequal(pars, parameters), 'Unexpected parameter struct returned') %% Test 2: choiceWorld @@ -19,6 +25,9 @@ pars = exp.inferParameters([expDefPath filesep 'choiceWorld.m']); load(fullfile(expDefPath, 'choiceWorld_parameters.mat')); +% Remove defFunction field before comparison +pars = rmfield(pars, 'defFunction'); +parameters = rmfield(parameters, 'defFunction'); assert(isequal(pars, parameters), 'Unexpected parameter struct returned') %% Test 3: single global parameter From 35a387060f523134ffddcdf8ff9b5d628df7f76d Mon Sep 17 00:00:00 2001 From: jaib1 Date: Fri, 15 Feb 2019 12:31:15 +0000 Subject: [PATCH 036/108] Rebased 'TestPanelClasses' onto 'dev' at diverge point c9c27a2 after pull request changes Test Panel as Classes modeled off SignalsExp & MControl variable name changes to exp.SignalsExp added 'cprintf.m' updated signals submodule updated signals submodule updates submodule signals updates signals submodule (TestPanelClasses branch) updated submodule signals (TestPanelClasses branch) MControl and mc commits to match dev branch alyx-related commits to match dev branch updated submodule signals (TestPanelClasses branch) updated submodule signals (TestPanelClasses branch) updated signals submodule (to 'TestPanelClasses' branch) and alyx-matlab(to 'dev' branch) and improved 'namedArg' updated signals submodule ('TestPanelClasses') after rebasing that branch in that submodule typo fix in 'hw.devices' made changes for Miles updated signals (TestPanelClasses) submodule Rebased 'TestPanelClasses' onto 'dev' at diverge point after pull request changes Rebased 'TestPanelClasses' onto 'dev' at diverge point Test Panel as Classes modeled off SignalsExp & MControl variable name changes to exp.SignalsExp added 'cprintf.m' updated signals submodule updated signals submodule updates submodule signals updates signals submodule (TestPanelClasses branch) updated submodule signals (TestPanelClasses branch) MControl and mc commits to match dev branch alyx-related commits to match dev branch updated submodule signals (TestPanelClasses branch) updated submodule signals (TestPanelClasses branch) updated signals submodule (to 'TestPanelClasses' branch) and alyx-matlab(to 'dev' branch) and improved 'namedArg' updated signals submodule ('TestPanelClasses') after rebasing that branch in that submodule typo fix in 'hw.devices' made changes for Miles updated signals (TestPanelClasses) submodule updated signals (TestPannelClasses) submodule after incorporating pull request changes undid variable name changes for Miles --- +eui/AlyxPanel.m | 20 ++++++++------ +eui/MControl.m | 2 +- +exp/SignalsExp.m | 2 ++ +exp/inferParameters.m | 55 ++++++++++++------------------------- +hw/devices.m | 6 ++-- alyx-matlab | 2 +- cb-tools/burgbox/namedArg.m | 2 +- cortexlab/+git/changes.m | 6 ---- signals | 2 +- 9 files changed, 39 insertions(+), 58 deletions(-) delete mode 100644 cortexlab/+git/changes.m diff --git a/+eui/AlyxPanel.m b/+eui/AlyxPanel.m index f8b6da14..ef221f98 100644 --- a/+eui/AlyxPanel.m +++ b/+eui/AlyxPanel.m @@ -217,6 +217,8 @@ function login(obj) % Logging out does not cause the token to expire, instead the % token is simply deleted from this object. + % Temporarily disable the Subject Selector + obj.NewExpSubject.UIControl.Enable = 'off'; % Reset headless flag in case user wishes to retry connection obj.AlyxInstance.Headless = false; % Are we logging in or out? @@ -282,6 +284,8 @@ function login(obj) notify(obj, 'Disconnected'); % Notify listeners of logout obj.log('Logged out of Alyx'); end + % Reable the Subject Selector + obj.NewExpSubject.UIControl.Enable = 'on'; obj.dispWaterReq() end @@ -314,7 +318,7 @@ function giveFutureWater(obj) 'enter space-separated numbers, i.e. \n',... '[tomorrow, day after that, day after that.. etc] \n\n',... 'Enter "0" to skip a day\nEnter "-1" to indicate training for that day\n']); - amtStr = inputdlg(prompt,'Future Amounts', [1 50]); + amtStr = newid(prompt,'Future Amounts', [1 50]); if isempty(amtStr)||~obj.AlyxInstance.IsLoggedIn return % user pressed 'Close' or 'x' end @@ -446,10 +450,10 @@ function recordWeight(obj, weight, subject) dlgTitle = 'Manual weight logging'; numLines = 1; defaultAns = {'',''}; - weight = inputdlg(prompt, dlgTitle, numLines, defaultAns); + weight = newid(prompt, dlgTitle, numLines, defaultAns); if isempty(weight); return; end end - % inputdlg returns weight as a cell, otherwise it may now be + % newid returns weight as a cell, otherwise it may now be weight = ensureCell(weight); % ensure it's a cell % convert to double if weight is a string weight = iff(ischar(weight{1}), str2double(weight{1}), weight{1}); @@ -601,13 +605,13 @@ function viewSubjectHistory(obj, ax) axWater = axes('Parent',plotBox); plot(axWater, dates, obj.round([records.given_water_total], 'up'), '.-'); hold(axWater, 'on'); - plot(axWater, dates, obj.round([records.given_water_hydrogel], 'down'), '.-'); - plot(axWater, dates, obj.round([records.given_water_liquid], 'down'), '.-'); + plot(axWater, dates, obj.round([records.given_water_supplement], 'down'), '.-'); + plot(axWater, dates, obj.round([records.given_water_reward], 'down'), '.-'); plot(axWater, dates, obj.round([records.expected_water], 'up'), 'r', 'LineWidth', 2.0); box(axWater, 'off'); xlim(axWater, [min(dates) maxDate]); set(axWater, 'XTickLabel', arrayfun(@(x)datestr(x, 'dd-mmm'), get(axWater, 'XTick'), 'uni', false)) - ylabel(axWater, 'water/hydrogel (mL)'); + ylabel(axWater, 'water (mL)'); % Create table of useful weight and water information, % sorted by date @@ -627,14 +631,14 @@ function viewSubjectHistory(obj, ax) arrayfun(@(x)iff(isnan(x), [], @()sprintf('%.1f', 0.8*(x-iw)+iw)), expected', 'uni', false), ... weightPctByDate'); waterDat = (... - num2cell(horzcat([records.given_water_liquid]', [records.given_water_hydrogel]', ... + num2cell(horzcat([records.given_water_reward]', [records.given_water_supplement]', ... [records.given_water_total]', [records.expected_water]',... [records.given_water_total]'-[records.expected_water]'))); waterDat = cellfun(@(x)sprintf('%.2f', x), waterDat, 'uni', false); waterDat(~[records.is_water_restricted],[1,3]) = {'ad lib'}; dat = horzcat(dat, waterDat); - set(histTable, 'ColumnName', {'date', 'meas. weight', '80% weight', 'weight pct', 'water', 'hydrogel', 'total', 'min water', 'excess'}, ... + set(histTable, 'ColumnName', {'date', 'meas. weight', '80% weight', 'weight pct', 'water', 'supplement', 'total', 'min water', 'excess'}, ... 'Data', dat(end:-1:1,:),... 'ColumnEditable', false(1,5)); histbox.Widths = [ -1 725]; diff --git a/+eui/MControl.m b/+eui/MControl.m index 5b25075d..0c432533 100644 --- a/+eui/MControl.m +++ b/+eui/MControl.m @@ -20,6 +20,7 @@ end properties (SetAccess = private) + AlyxPanel % holds the AlyxPanel object (see buildUI(), eui.AlyxPanel()) LogSubject % Subject selector control NewExpSubject % Experiment selector control NewExpType % Experiment type selector control @@ -33,7 +34,6 @@ properties (Access = private) ParamEditor ParamPanel - AlyxPanel % holds the AlyxPanel object (see buildUI(), eui.AlyxPanel()) BeginExpButton % The 'Start' button that begins an experiment RigOptionsButton % The 'Options' button that opens the rig options dialog NewExpFactory % A struct containing all availiable experiment types and function handles to constructors for their default parameters diff --git a/+exp/SignalsExp.m b/+exp/SignalsExp.m index f616ea70..d2fb2631 100644 --- a/+exp/SignalsExp.m +++ b/+exp/SignalsExp.m @@ -720,6 +720,8 @@ function mainLoop(obj) obj.Data.stimWindowRenderTimes(obj.StimWindowUpdateCount) = renderTime; obj.StimWindowInvalid = false; end + % make sure some minimum time passes before updating signals, to + % improve performance on MC if (obj.Clock.now - t) > 0.1 || obj.IsLooping == false sendSignalUpdates(obj); t = obj.Clock.now; diff --git a/+exp/inferParameters.m b/+exp/inferParameters.m index 945845d1..275c964f 100644 --- a/+exp/inferParameters.m +++ b/+exp/inferParameters.m @@ -5,12 +5,6 @@ % create some signals just to pass to the definition function and track % which parameter names are used -% if ischar(expdef) && file.exists(expdef) -% expdeffun = fileFunction(expdef); -% else -% expdeffun = expdef; -% expdef = which(func2str(expdef)); -% end if ischar(expdef) && file.exists(expdef) expdeffun = fileFunction(expdef); else @@ -18,38 +12,28 @@ expdef = which(func2str(expdef)); end -net = sig.Net; -e = struct; -e.t = net.origin('t'); -e.events = net.subscriptableOrigin('events'); -e.pars = net.subscriptableOrigin('pars'); -e.pars.CacheSubscripts = true; -e.visual = net.subscriptableOrigin('visual'); -e.audio.Devices = @dummyDev; -e.inputs = net.subscriptableOrigin('inputs'); -e.outputs = net.subscriptableOrigin('outputs'); +e = sig.void; +pars = sig.void(true); +audio.Devices = @dummyDev; try + expdeffun(e.t, e.events, pars, e.visual, e.inputs, e.outputs, audio); - expdeffun(e.t, e.events, e.pars, e.visual, e.inputs, e.outputs, e.audio); - - % paramNames will be the strings corresponding to the fields of e.pars + % paramNames will be the strings corresponding to the fields of pars % that the user tried to reference in her expdeffun. - paramNames = e.pars.Subscripts.keys'; - %The paramValues are signals corresponding to those parameters and they - %will all be empty, except when they've been given explicit numerical - %definitions right at the end of the function - and in that case, we'll - %take those values (extracted into matlab datatypes, from the signals, - %using .Node.CurrValue) to be the desired default values. - paramValues = e.pars.Subscripts.values'; - parsStruct = cell2struct(cell(size(paramNames)), paramNames); - for i = 1:size(paramNames,1) - parsStruct.(paramNames{i}) = paramValues{i}.Node.CurrValue; - end + parsStruct = pars.Subscripts; + + % Check for reserved fieldnames + reserved = {'randomiseConditions', 'services', 'expPanelFun', ... + 'numRepeats', 'defFunction', 'waterType', 'isPassive'}; + assert(~any(ismember(fieldnames(parsStruct), reserved)), ... + 'exp:InferParameters:ReservedParameters', ... + 'The following param names are reserved:\n%s', ... + strjoin(intersect(fieldnames(parsStruct), reserved), ', ')) + + szFcn = @(a)iff(ischar(a), @()size(a,1), @()size(a,2)); sz = iff(isempty(fieldnames(parsStruct)), 1,... % if there are no paramters sz = 1 - structfun(@(a)size(a,2), parsStruct)); % otherwise get number of columns - isChar = structfun(@ischar, parsStruct); % we disregard charecter arrays - if any(isChar); sz = sz(~isChar); end + structfun(szFcn, parsStruct)); % otherwise get number of columns % add 'numRepeats' parameter, where total number of trials = 1000 parsStruct.numRepeats = ones(1,max(sz))*floor(1000/max(sz)); parsStruct.defFunction = expdef; @@ -60,12 +44,9 @@ ExpPanel_fn = [path filesep ExpPanel_name ext]; if exist(ExpPanel_fn,'file'); parsStruct.expPanelFun = ExpPanel_name; end catch ex - net.delete(); rethrow(ex) end -net.delete(); - function dev = dummyDev(~) % Returns a dummy audio device structure, regardless of input % Returns a standard structure with values for generating tone @@ -75,4 +56,4 @@ 'DefaultSampleRate', 44100,... 'NrOutputChannels', 2); end -end \ No newline at end of file +end diff --git a/+hw/devices.m b/+hw/devices.m index 2734fe92..1bba5908 100644 --- a/+hw/devices.m +++ b/+hw/devices.m @@ -69,8 +69,8 @@ % Get list of audio devices devs = getOr(rig, 'audioDevices', PsychPortAudio('GetDevices')); % Sanitize the names - names = matlab.lang.makeValidName([{'default'} {devs(2:end).DeviceName}],... - 'ReplacementStyle', 'delete'); + names = matlab.lang.makeValidName({devs.DeviceName}, 'ReplacementStyle', 'delete'); + names = iff(ismember('default', names), names, @()[{'default'} names(2:end)]); for i = 1:length(names); devs(i).DeviceName = names{i}; end rig.audioDevices = devs; end @@ -93,4 +93,4 @@ function configure(deviceName, usedaq) end end -end \ No newline at end of file +end diff --git a/alyx-matlab b/alyx-matlab index dd2ab36d..55e3e2ad 160000 --- a/alyx-matlab +++ b/alyx-matlab @@ -1 +1 @@ -Subproject commit dd2ab36de59843edc94c15956967731d81173b74 +Subproject commit 55e3e2adc57c42a1c84dbc0b8570c428f246f65d diff --git a/cb-tools/burgbox/namedArg.m b/cb-tools/burgbox/namedArg.m index 5e5820fb..72264f8a 100644 --- a/cb-tools/burgbox/namedArg.m +++ b/cb-tools/burgbox/namedArg.m @@ -8,7 +8,7 @@ % 2014-02 CB created -defIdx = find(cellfun(@(a) isequal(a, name), args), 1); +defIdx = find(cellfun(@(a) strcmpi(a, name), args), 1); if ~isempty(defIdx) present = true; value = args{defIdx + 1}; diff --git a/cortexlab/+git/changes.m b/cortexlab/+git/changes.m deleted file mode 100644 index d9e9f013..00000000 --- a/cortexlab/+git/changes.m +++ /dev/null @@ -1,6 +0,0 @@ -disp('Updating queued Alyx posts...') -posts = dirPlus(getOr(dat.paths, 'localAlyxQueue', 'C:/localAlyxQueue')); -posts = posts(endsWith(posts, 'put')); -newPosts = cellfun(@(str)[str(1:end-3) 'patch'], posts, 'uni', 0); -status = cellfun(@movefile, posts, newPosts); -assert(all(status), 'Unable to rename queued Alyx files, please do this manually') \ No newline at end of file diff --git a/signals b/signals index 93520307..ea8f85fe 160000 --- a/signals +++ b/signals @@ -1 +1 @@ -Subproject commit 93520307c1cb1a9dc12fa8bda685a01b9ab80401 +Subproject commit ea8f85fe28313ca9562f9a52ff58114b3f7ef6de From 86fcbbbe86861182737cd09501611382a2bd4e75 Mon Sep 17 00:00:00 2001 From: jaib1 Date: Thu, 7 Mar 2019 11:05:17 +0000 Subject: [PATCH 037/108] updated signals (dev) submodule --- signals | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/signals b/signals index 8a56f9e6..01829db3 160000 --- a/signals +++ b/signals @@ -1 +1 @@ -Subproject commit 8a56f9e6b3b5cf0d37c34e5b0b948c2e55bb45d8 +Subproject commit 01829db30c43561a0ec7481b14cfa895e31c59bb From b6b47dba83c449c464fc8f1bc1f79efc0957b3ca Mon Sep 17 00:00:00 2001 From: jaib1 Date: Thu, 7 Mar 2019 12:35:30 +0000 Subject: [PATCH 038/108] updated signals (TestPanelClasses) submodule --- signals | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/signals b/signals index ea8f85fe..38007424 160000 --- a/signals +++ b/signals @@ -1 +1 @@ -Subproject commit ea8f85fe28313ca9562f9a52ff58114b3f7ef6de +Subproject commit 380074249abfd91b9f0be65fb3a9f5eb5b5944d6 From 6238fda95e8c724f6975d7ee3adfa3f2ffc1a5ea Mon Sep 17 00:00:00 2001 From: jaib1 Date: Thu, 7 Mar 2019 12:40:00 +0000 Subject: [PATCH 039/108] updated signals (dev, merged into from TestPanelClasses) submodule --- signals | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/signals b/signals index 38007424..2350bac3 160000 --- a/signals +++ b/signals @@ -1 +1 @@ -Subproject commit 380074249abfd91b9f0be65fb3a9f5eb5b5944d6 +Subproject commit 2350bac34f61f0cecab49e7fcd9fa6c055157262 From 5be5059e4a336468534be169de98e39212d9940e Mon Sep 17 00:00:00 2001 From: k1o0 Date: Fri, 8 Mar 2019 17:06:30 +0200 Subject: [PATCH 040/108] Added tests and bug fix for timer deletion --- +eui/AlyxPanel.m | 3 +- tests/AlyxPanelTest.m | 133 +++++++++++++++++++++++++++++++++ tests/data/viewSubjectData.mat | Bin 0 -> 8840 bytes 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 tests/AlyxPanelTest.m create mode 100644 tests/data/viewSubjectData.mat diff --git a/+eui/AlyxPanel.m b/+eui/AlyxPanel.m index 53d573fc..7e4306d2 100644 --- a/+eui/AlyxPanel.m +++ b/+eui/AlyxPanel.m @@ -76,7 +76,8 @@ 'Toolbar', 'none',... 'NumberTitle', 'off',... 'Units', 'normalized',... - 'OuterPosition', [0.1 0.1 0.4 .4]); + 'OuterPosition', [0.1 0.1 0.4 .4],... + 'DeleteFcn', @(~,~)obj.delete); parent = uiextras.VBox('Parent', f,... 'Visible', 'on'); % subject selector diff --git a/tests/AlyxPanelTest.m b/tests/AlyxPanelTest.m new file mode 100644 index 00000000..b220d9cb --- /dev/null +++ b/tests/AlyxPanelTest.m @@ -0,0 +1,133 @@ +classdef AlyxPanelTest < matlab.unittest.TestCase + + properties + % Figure visibility setting before running tests + FigureVisibleDefault + % AlyxPanel instance + Panel + % Figure handle for AlyxPanel + hPanel + % Figure handle for any extra figures opened during tests + Figure + % List of subjects returned by the test database + Subjects = {'ZM_1085'; 'ZM_1087'; 'ZM_1094'; 'ZM_1098'; 'ZM_335'} + % bui.Selector object for setting the subject list in tests + SubjectUI + % Expected Y-axis labels for the viewSubjectHistory plots + Ylabels = {'water (mL)', 'weight as pct (%)', 'weight (g)'} + % The table data for the the viewSubjectHistory table + TableData + % Cell array of graph data for the the viewSubjectHistory plots. One + % cell per plot containing {xData, yData} arrays. + GraphData + end + + methods (TestClassSetup) + function killFigures(testCase) + testCase.FigureVisibleDefault = get(0,'DefaultFigureVisible'); + set(0,'DefaultFigureVisible','off'); + end + + function loadData(testCase) + % Loads validation data + % Graph data is a cell array where each element is the graph number + % (1:3) and within each element is a cell of X- and Y- axis values + % respecively + load('data/viewSubjectData.mat', 'tableData', 'graphData') + testCase.TableData = tableData; + testCase.GraphData = graphData; + end + + function setupPanel(testCase) + % Check paths file + assert(endsWith(which('dat.paths'), fullfile('tests','+dat','paths.m'))); + % Create figure for panel + testCase.hPanel = figure('Name', 'alyx GUI',... + 'MenuBar', 'none',... + 'Toolbar', 'none',... + 'NumberTitle', 'off',... + 'Units', 'normalized',... + 'OuterPosition', [0.1 0.1 0.4 .4]); + parent = uiextras.VBox('Parent', testCase.hPanel, 'Visible', 'on'); + testCase.Panel = eui.AlyxPanel(parent); + % subject selector + sbox = uix.HBox('Parent', parent); + bui.label('Select subject: ', sbox); + % Subject dropdown box + testCase.SubjectUI = bui.Selector(sbox, [{'default'}; testCase.Subjects]); + % set a callback on subject selection so that we can show water + % requirements for new mice as they are selected. This should + % be set by any other GUI that instantiates this object (e.g. + % MControl using this as a panel. + testCase.SubjectUI.addlistener('SelectionChanged', ... + @(src, evt)testCase.Panel.dispWaterReq(src, evt)); + + % Set Alyx Instance and log in + testCase.Panel.login('test_user', 'TapetesBloc18'); + testCase.fatalAssertTrue(testCase.Panel.AlyxInstance.IsLoggedIn,... + 'Failed to log into Alyx'); + end + end + + methods (TestClassTeardown) + function restoreFigures(testCase) + set(0,'DefaultFigureVisible',testCase.FigureVisibleDefault); + close(testCase.hPanel) + end + end + + methods (TestMethodTeardown) + function closeFigure(testCase) + % Close any figures opened during the test + if ~isempty(testCase.Figure); close(testCase.Figure); end + end + end + + methods (Test) + function test_viewSubjectHistory(testCase) + % Post some weights for plotting + + % Set new subject + testCase.SubjectUI.Selected = testCase.SubjectUI.Option{3}; + testCase.Panel.viewSubjectHistory + testCase.Figure = gcf(); + child_handles = testCase.Figure.Children.Children; + % Verify table data + testCase.assertTrue(isa(child_handles(1),'matlab.ui.control.Table')); + tableData = child_handles(1).Data; + testCase.verifyTrue(isequal(size(tableData), [ceil(now-737146) 9]), ... + 'Unexpected table data'); + expected = testCase.TableData; + % Remove empty days + idx = find(strcmp(expected{1,1}, tableData(:,1)),1); + tableData = tableData(idx:end,:); + testCase.verifyTrue(isequal(tableData, expected), 'Unexpected table data'); + + ax_h = child_handles(2).Children; + testCase.assertTrue(isa(ax_h, 'matlab.graphics.axis.Axes')) + testCase.assertTrue(length(ax_h)==3, 'Not all axes created') + + for i = 1:length(ax_h) + label = testCase.Ylabels{i}; + testCase.verifyEqual(ax_h(i).YLabel.String, label); + testCase.verifyEqual(length(ax_h(i).Children), ... + size(testCase.GraphData{i}{1},1)); + xData = vertcat(ax_h(i).Children(:).XData); + yData = vertcat(ax_h(i).Children(:).YData); + testCase.verifyEqual(xData, testCase.GraphData{i}{1}); + testCase.verifyEqual(yData, testCase.GraphData{i}{2}); + end + end + + function test_viewAllSubjects(testCase) + testCase.Panel.viewAllSubjects; + testCase.Figure = gcf(); + child_handle = testCase.Figure.Children.Children; + tableData = child_handle.Data; + expected = {'algernon', '0.00', '0.00'}; + testCase.verifyTrue(isequal(tableData, expected)); + end + + end + +end \ No newline at end of file diff --git a/tests/data/viewSubjectData.mat b/tests/data/viewSubjectData.mat new file mode 100644 index 0000000000000000000000000000000000000000..7bd663b95c8630a659a07ceec01f7ad4a4d5371f GIT binary patch literal 8840 zcma)hcT`i~voF0j6$Gh4K~Rvc1PFv8AW~I&FDlY&gisPXh#(>;pg<@hMLH;*&{0YP z(tDKvq4y+&Jiq1L_j~vLbN5!W>pr9PjHYn_f0AR_<-!z5RP+4+r7%gskA5226JF!ho2- z>+Y*eN!<^HT{EH_tNdp?U!#TZjcs4ymiHR1)!Ej%!|kzYrsD9l3<+*MySgAkfE!m; z!f)Zz^os11?+|^~FtyDv^V~4y)cPyi z^~BzNs_gp$N1EWHUNWXG08fV?#{Q7OMx_KM=C8xcC8f!P!gvUjgvIXiZuBZ zX?n?JVZ9!9y>2EoeY%a-IzY+5m zF7FttrMF1n;K9^#yY-5gCW3EyVfJb?lv1K2a-IlT**}5Kk z!4($H71hoaksHWh=)~nK#fx6FNN9VI(D4Eki`l2>u@~=g0Nu2|esf8)IqGM6L~(k| zMtXSKK7(zyTly=D^hAa9*U3JY~32tw-Qej_o^a#Xw*!OBmuQ-(_n!AS{IyKrguALhLv#PLM7a%>! zZk-Qgcl|PZsMHF@x0WWUGZQL{X0hm66_Y1C*ZHHlcQhVhG5fZ5UqBB^eosw3RMs)Tzp*nXCu7JDE3})g69^J5l1Q z95?e-xyDBGl3_lfIJwUzbRT(<@}OT9G8tF>+*C3qnznYfx1Xnj(o82;txhWzX!2V! zwnJtdo(bGMKKpr8O$g-0ZM^VmxGw9mc*udYRMD4YOo<`hfD5EDVr~i}M)An(*w8$9 zymnB4mE|fC)laNMd^*a*-|+3$<*++KdG;I*d>#;(NTx-~rUackm@AqCNYOe!(+ut? zTcxLB@)vYJ9lS+1=*%1(_8oArq8U>TLNu%!bbh*&q(HD4@NE&{LTKi(nayiY&&T6- z_3{}yCeF+A#Nz{jQa_b!==4}eee$4 z=%U#yz4ju+$?j_s*ltcNNq*2Pl$2zBEG}H=@lh-Z8_3ZUM-*Ap_KQHK-P<;7M%t1N zfV&G%aMSw(MeyHaK6dc*)XZaT)P;`0ArPGgt5H!PwGayZ@VN208@CSo&wjXgGz5UJ z31Ao%kai!-z4~4Bkx7#R4M=$~E_4lt@xc{4!w^0#W{9l{ee8_V!uS3CkRKU#zFs5` zzrhViCe8tl39*OUlh#5;l`lENZ~S>{;``mxHChD%nD8Xst>Pwe7l#O3ekGFq7isk< zCv^({&HhEa;^E|f$H64MaiR-v?|!X0!!?T-o%ox~j!|z5Xi}c_ba8dQEqnakJ<&(| z_Ge8|;YSaJ-wO*qnl7a;-g1(UyCi*=t7md&TdEb*f|(888BmbL3YB{k=A=NvcnR*S@PVxJP?b@;!E- z{oHOaNxkuwyh-p%HGsz#zS7b#sVag+k*6jn)uvLOYeEs;*RYQah7nxIzd;SuS%eHRB7~?WHH{WFLku zDXKYFMo0{CG=|^xkL4e_5){rn+2K6IgIBdXd_)uz5`Q2w2u@&Z##Xa14Vt9m|m2Q)U_{d#@>-GQhd+b^wOz! z_LK)=1ngOUENZ7S@=LLT+8LW2RK&xg7b#!2ah|scP}oaTbzC-Q=m*e6I4DG^ik5~+ znbR*-GMH*m-S6Ozi+EwqD9y&`Q^~UIpe&?%U8tRAlJ1ketWf9G2jOLv%-=O`Z-vQC z(l-J4WbEais$YGo!Lr)HKgme22lIwSzNL)?2(bLAtPIrNOboen@k_wWuoQ0wgU(MF z)@wa|V0e)*r~Ubw!h_;O-QPag^VqZ;#=YEa8AC>%$M@&;^im0CfJoZE?E1>$Nn~ zOUBn%P0eE1RklUH?_LY_>AQ4AI*q$d(U#^QDV1SczCPI8UrBTg1|<&8Rd=6%S#~?- zM2o!-Q1)>RFsM%#v;5Tb3TjZlbUd!n-FzT3&xZAJE#*>>u4uN_?Xp&RaqENPG6el5 zDP+}B;=36Hmd&`mBBf@A!(P9OD4PoyQ?LM_W>VkMSj_Nsog-`mf+RjPRO>F+>wNQi z{z3VLdEUWx!2uOoXu(r+^E7|+?CW$A>bFDqH<@mtAf&;|%K4AEo0zlpsIe-GJ~RU4 z(f!IK%gb^8OZ5$(3v^!*@$=J|bVLf|VBS=4Cxud7|F>ink*j-xyXYMM&eblHM@nJ( z2~M$K^N}%lKiV8wva)6;wa~hAy9!=>{yhIuHwx!iZX;6|JR2xIks$3}IHpSsy|vu_ zV}MhsWD=1zaL2#*RRhe>s=`0=_6hVAKE>vVSGrcQUE!mzZn`VKQjrpoOp=Nm`j^mF z!``Lm1u*4zxS{lo=$bA_*(RP3Hg5Q`ES|tzjLUb;GS!M6EIeVuCQr+M9v@!ZHC`V^g{7rqDK!kdD-aw2S!#i{EhgvpZibDh4j?`Z+es-;pU{ z+ohGs+)d3$yn1-DkJl_H-&_Ev54gyh5`N7bUoiZgSx0Ei2ON0Y_LXkDdw1#fjbPzm zj`C7Alz}M2d%CIA``3lVWP?AvSC+Z1^zgBcsg_oA0W*?3q%qpJ} zk?3F~1|IO{O$PS+@&orVhui}~d`@(Rn90&vN^9{(yU}T-;2>%q)c&OIO{OpqlBl^ zkpG+SY6RKV+N{%Q|EfMw0CTF>4)o&D&nkIM+a8)ze|2Ckjkc!?hg9BRk2P4WQTC{?D+XmcQ~ z4;w4JH~-}&3ZZt*daAS~2J7gj^)wSpG_z6c_0)AsEVI$Z4GPeL>>UzO*)Lb~&Z|xTo zcD&#E%x4nZ`DX(5L$}B_S!I3MBJT@pX;ZNWdy~;o(uVuyPynfnDPqHKFVcCl$C34h zt+dfE3VAaCnKQ{z6qqpA92y|2HJJp(w>IHL86#S2^38CX#oI%4)|4N^ux-pa*XrqK zDJ;Wg+V~bFh8Swew^eDqS=XuZ+Cmq}_t*ldKs4E8hT+a_emRWmG`3{QZMr#}>y)739@!{)pqTST$vCC^K%-LuLPxE%@O2cNLsl(^~{F3LyU^TfLnJoz$ex3Q$ z!)P_PmV0eSj@-6i3Rx!7XvBzmUZTujSU7!b0uWX2uuZ?LOcT!!EYMADo&J_ywD9b; zvQr(bW5fL5EP6Bf?bS;YKNI^PeHzG6o3tm+`=A#VbOoeEG{>xhRx@1BjzcF1$z$<9giRL5$cX)*^K_^e9a6SMSc^2h7O`f zaG)6+tn%o|w$uK|%CpNhOO57|6F*5>P`+nqtBHPCW_92Xv_FQ>x21g~c`7-w0peZ5 zzpQ=-oahn|nbTH={cN@_yvRaBEY6pkzk4zQXnav4_fg+W* z?R;2+3J4%N`?5AiX@`X2 zE$7_qin3W`ebN@z5v(^$`1CAuA;jHw1yqF+nb1^$W)~-Y7m|SGR}41#pk~Grccbrj z>&(<7qTP-veyfNt1qt#|p;l^3S1v>O#j#dh5C)MB2p%>4$B5K*M$Izp zup}tMvM+IWDrBtm$!rQp&rX}3LCPjX9+x4p_8!hTnV8a=)YkxCX+f3pkaW`_jYut$ z-GvR-a-wnkTREi6%?3Mg@%Jp@WKR|@K)R;2@hB*jzaSRKOqLd0uKd^emFgc7m{~e5 z{jkf3J!Y8hc09mS$eWZlptr?d>Mm~GF6wy0QM$ZCzn(qqc^91{mfpI95o!mC51xk1 z5oT-yfXJk^DQqhu^7pwkdjEikZJC)@R<`yCnE8zyfl9358jvK+#Y9Vr-CWIYYbUrL z5^MDgZ|AI<6{9Gfm5ZN=O(O+ND{Sf&)u?6N-7Gb7zMn-7;ohWzyu|>}YT1z>S}4l_ z@2bKNzkdXbP>I!sw;1*8|6urk*|n;?Yl-$>Yw7mCIXabt;)a+O$>yQL6q*0v#Qb-R ze?YB&_z9w;L7WHwk8xdi04L(_pIrTwae8$m!^k;O(QVWs7Zs;SxhXfQmd*2@mi&il z{~hOt-_OvMYuh||vbiW4$aqnWde$Wf&8SP!l|LZ=p?4LH_&cGFrt3cJQf1e)dX)%> zc;q7CCj{Mc;sl*wd`6NFj6xQeJ zrwX6Db9M=b&l`7o=KOaXLqg!UQegc$Y`tvSOxl!PKiLXnfqY#qf(Dd600cEo{0CgT3RuG99-pN(57}Ulr5cA{tJBb;^BwJ%RxWHpR1fkhO>Hr~U1i?#;zX zMK|;WbW^cZxNeUi`(A0}^iyaZqo0iwCjr^z=OPRN9=G1d&XZ1I*)Du}glmIkCty-x z=mVA$UB=A|`o9X>Y(_szB!Erxn~T1gJ~FDvxQj%p`eiKN5s|saB_!y&%nS_R52)O2 z+}U})KCv`v~`C@rrWHj4t31oQ4xT4Iatq#p; zjP22|nEPlH`*bZFCo++2T=$s&-A2K{h0gX*|EKEw_wf+G?uT(ni;0>cJ9nOAHLU-+ z&Uvem^5sVKCedH4*%>9NCs}5+>)8$U$}bw4890p*!Y19% zmK;_I=^)A}dlf*_PIko&#etI27x%8#1x1lo*6^75U*2~AvL3$i30M&AKV z+Sujx#sGHKaRn@YDOv~q3{#se!l2En2n-r(mp0^s=7l5~q4JT{ex@yUdzghoiGQq3 z$HsR8*tABTS)G_oiCprN9zKOvWeH!M=3e(Mx=TYT$mMH<`Q-E5ccXDfdq=m<76WXx zDLgxmM}M8Y2P2W@POk*S?|RNwB*Y9yIhTLHxh@sis0==vTX4tU_MbF&Ug55DF7MV@ zdGllJ-GbYaoZ{I}rZ~UnQM@H~=b7bxX(!eL6bfG=oeu{+6TzeiC9pTPzHWR{e)@Pl z^I>m$;17=7id0NOG_R7g7FTShST;Hpnk zA@LMGAdx+#xu$_h7|?FGjgPwn?&oC{N4lSVt48SJv$8hB@G?OLLxF63=`amT&YefJ z`-a9y2_90h%&C$dIBoofc3<78&k^n4Eo0gOLkEDp=Z>pChqAgMz`$d3g?)~yg#;0K z&iU+Zh`ygcjg#M2ufkat{H1yv^PHHpEwr5vx(bBlCG$*%RE=gUXjgi7gR1Fq9T;Xg+7QH_J4*;SX!y za`85h&tYklDQxr0qCh9RHbU>n3gjqc0YyimUD_4q!eNSfv1FT^ zM?by|rKJa>f3H7ysJcHm3-?1Dv|d3&;dTOO0`_-mK)HJs;!57#?Eg)!atz)P>nD83G%M;?@o zSl_MsW~q5$y?)BK<7-w{|6*yab)!1hOS5%x;oTLM*v=(yAn$+Olos||1MH-q& zgBLq>7}K4;rXR0gXN_y`9@722yHkS<9a3ymiww83yF`*}AAQ{=Inr#6McO?R$}y^p zc1YPmZ5f_)O7TwD@Yy2g4q7c*JwEIfo|vKvcxOnzvWc|y`=IOPv%~XI^`91xtS{=l z2rbr%QwtnEvdcZCEyaxdWk7U1CJ!bu%g|y!6iE*%vg%A@ zkCtgGYHt4a@P|XedX3;w=vVNVtz_d-WaDBuaL|cc(k1gi)bdm=coNJdA%IBpUr6q8 zu#w7F?lgIWE%7OXO`ID*e6^1s*BV0ROj_MC3w%l^(hHA49_9O##8F(vjv+yMo8+Bk z0{slME<8|!*dE3&|AN`Kijn;1<3olZ)jMA%rgRXUrzT5{ZwNC-EzxTns{)7N<5T^h zr`^1oE3zQY=Pl#GQQojXxP;-qDX0H0_i5g!?S6ko4^#V*f3mL-&(lu&vx)Ma>;`)e zsN(Pn12Q>btz9*{t`}60pO# z2!yjRDlm>s{?#e{O7ywMlLzv z#>Gi~oMgFaszxddx-_k8a-`jo{V+9P66390cx;l8kC%I_EdG0mHk5bl)xaU^X6VN{ z$Uw3vt97jKhV`3lL$pR7CK8gp@x*nv!Y!lx_aQULIdA`S&oC%u^Lh>KE+C_@`nkVkGpcHQJ{5&1LDiYQ@ zRK&Ew4``oS%TV*{iOBsOnjgb~VaMbX99(c9|Jy8-;_B(+dZy4{tznx9YuNGxd>C4D zmi2K<vg?X$8o~u6@1n__yuAv*N^_CDyG`lrlk3MxpQ+DMY=kIdY?wEL5fKxk)(N98mD`xXxZzlron#Bo>?&9AQMp}>EcK-Pd zk@gC&FH?QEQRVke7Qp{ud(AAsrx9zmTksG7@e>R*_%EqR)Eeu4sE#h zQ}%~&x-4zM4_Q>jNyuU3c6Y#|ED0}mkHfCZ}auX-8QD;~EjqcbA`*Ol z6R~aAU-+3Civ@jMMOMO{xk~>;yiN|;ZF^o zmD+z~k3u&};(~4$l!mq(AJ0!1tAKln#7eNh$pV@{puYRzA{5`7c18^*wiyb$4ji@e z@#f9Ri(LGKMaYX17%nJc-Sn4mC41*lBB=fuBAsWDoqJ!a{wh|ioW9kQB~y^Jb7MK; zh_$Oxo(W+BHk!~};%RP4o}aL71LJU1)fGb*OXw*a2No-zT^TIw^I8M(3Uv3|?gEKU zMJY*mUU=?`m5jlrSjk0fT$=y=^-~qQ5Oiy!99acEaRSqy`ATE%2a$UUBPE1*f{m*) z6b(@;6=3sMD0b!!MhmT^_vrZ1{*?R`LZ`7NywjLnR%hHa2J+pwpN}j@`5lJRJXH?& zLX{6v{|U6Y{j%wb6LOt2rd9C13Q)+fn$Xf$qhv65_9*D*V`n4hV^X$RhF`-uX(&tJ z@2~gwhdG8$0F&08S|_st$zN}c38xp5-xC^N+a?}cKXpH@YyztgoJ}^;4$Y^INs(92I!6ESKA4Nr${5>@L@h^ z%*+46h&XbV5B-Ql+QRdXIR{71wU1NAcO{1e5)tDFvvUNgsnmK&Z~R(wn#E6;K7(Qf Q9Ak2j5=>Ti*GKdJ03HqOoB#j- literal 0 HcmV?d00001 From 447eb42415e4046364da2deac6606ce2c4f34a43 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Fri, 8 Mar 2019 18:06:04 +0200 Subject: [PATCH 041/108] Last weight used in calculation for subject history plots --- +eui/AlyxPanel.m | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/+eui/AlyxPanel.m b/+eui/AlyxPanel.m index 7e4306d2..e8b24c6c 100644 --- a/+eui/AlyxPanel.m +++ b/+eui/AlyxPanel.m @@ -554,8 +554,10 @@ function viewSubjectHistory(obj, ax) obj.log('No weight data found for subject %s', obj.Subject); return end + weights = [records.weight]; + weights(isnan([records.weighing_at])) = nan; expected = [records.expected_weight]; - expected(expected==0|isnan([records.weighing_at])) = nan; + expected(expected==0|isnan(weights)) = nan; dates = cellfun(@(x)datenum(x), {records.date}); % build the figure to show it @@ -568,13 +570,13 @@ function viewSubjectHistory(obj, ax) ax = axes('Parent', plotBox); end - plot(ax, dates, [records.weighing_at], '.-'); + plot(ax, dates, weights, '.-'); hold(ax, 'on'); plot(ax, dates, ((expected-iw)*0.7)+iw, 'r', 'LineWidth', 2.0); plot(ax, dates, ((expected-iw)*0.8)+iw, 'LineWidth', 2.0, 'Color', [244, 191, 66]/255); box(ax, 'off'); % Change the plot x axis limits - maxDate = max(dates([records.is_water_restricted]|~isnan([records.weighing_at]))); + maxDate = max(dates([records.is_water_restricted]|~isnan(weights))); if numel(dates) > 1 && ~isempty(maxDate) && min(dates) ~= maxDate xlim(ax, [min(dates) maxDate]) else @@ -590,7 +592,7 @@ function viewSubjectHistory(obj, ax) if nargin==1 ax = axes('Parent', plotBox); - plot(ax, dates, ([records.weighing_at]-iw)./(expected-iw), '.-'); + plot(ax, dates, (weights-iw)./(expected-iw), '.-'); hold(ax, 'on'); plot(ax, dates, 0.7*ones(size(dates)), 'r', 'LineWidth', 2.0); plot(ax, dates, 0.8*ones(size(dates)), 'LineWidth', 2.0, 'Color', [244, 191, 66]/255); @@ -602,25 +604,25 @@ function viewSubjectHistory(obj, ax) axWater = axes('Parent',plotBox); plot(axWater, dates, obj.round([records.given_water_total], 'up'), '.-'); hold(axWater, 'on'); - plot(axWater, dates, obj.round([records.given_water_hydrogel], 'down'), '.-'); - plot(axWater, dates, obj.round([records.given_water_liquid], 'down'), '.-'); + plot(axWater, dates, obj.round([records.given_water_supplement], 'down'), '.-'); + plot(axWater, dates, obj.round([records.given_water_reward], 'down'), '.-'); plot(axWater, dates, obj.round([records.expected_water], 'up'), 'r', 'LineWidth', 2.0); box(axWater, 'off'); xlim(axWater, [min(dates) maxDate]); set(axWater, 'XTickLabel', arrayfun(@(x)datestr(x, 'dd-mmm'), get(axWater, 'XTick'), 'uni', false)) - ylabel(axWater, 'water/hydrogel (mL)'); + ylabel(axWater, 'water (mL)'); % Create table of useful weight and water information, % sorted by date histTable = uitable('Parent', histbox,... 'FontName', 'Consolas',... 'RowName', []); - weightsByDate = num2cell([records.weighing_at]); + weightsByDate = num2cell(weights); weightsByDate = cellfun(@(x)sprintf('%.1f', x), weightsByDate, 'uni', false); - weightsByDate(isnan([records.weighing_at])) = {[]}; - weightPctByDate = num2cell(([records.weighing_at]-iw)./(expected-iw)); + weightsByDate(isnan(weights)) = {[]}; + weightPctByDate = num2cell((weights-iw)./(expected-iw)); weightPctByDate = cellfun(@(x)sprintf('%.1f', x*100), weightPctByDate, 'uni', false); - weightPctByDate(isnan([records.weighing_at])|~[records.is_water_restricted]) = {[]}; + weightPctByDate(isnan(weights)|~[records.is_water_restricted]) = {[]}; dat = horzcat(... arrayfun(@(x)datestr(x), dates', 'uni', false), ... @@ -628,14 +630,14 @@ function viewSubjectHistory(obj, ax) arrayfun(@(x)iff(isnan(x), [], @()sprintf('%.1f', 0.8*(x-iw)+iw)), expected', 'uni', false), ... weightPctByDate'); waterDat = (... - num2cell(horzcat([records.given_water_liquid]', [records.given_water_hydrogel]', ... + num2cell(horzcat([records.given_water_reward]', [records.given_water_supplement]', ... [records.given_water_total]', [records.expected_water]',... [records.given_water_total]'-[records.expected_water]'))); waterDat = cellfun(@(x)sprintf('%.2f', x), waterDat, 'uni', false); waterDat(~[records.is_water_restricted],[1,3]) = {'ad lib'}; dat = horzcat(dat, waterDat); - set(histTable, 'ColumnName', {'date', 'meas. weight', '80% weight', 'weight pct', 'water', 'hydrogel', 'total', 'min water', 'excess'}, ... + set(histTable, 'ColumnName', {'date', 'meas. weight', '80% weight', 'weight pct', 'water', 'supplement', 'total', 'min water', 'excess'}, ... 'Data', dat(end:-1:1,:),... 'ColumnEditable', false(1,5)); histbox.Widths = [ -1 725]; From df607ec3ae2d42d6b0ef1543156406d13a90e10e Mon Sep 17 00:00:00 2001 From: k1o0 Date: Sun, 10 Mar 2019 12:56:02 +0200 Subject: [PATCH 042/108] Revert irrelevent commits --- +eui/ConditionPanel.m | 254 ---------------- +eui/FieldPanel.m | 165 ----------- +eui/MControl.m | 24 +- +eui/ParamEditor.m | 632 +++++++++++++++++++++++++++------------- +eui/ParamEditor_old.m | 516 -------------------------------- +exp/Parameters.m | 10 +- cortexlab/+git/update.m | 2 +- signals | 2 +- 8 files changed, 438 insertions(+), 1167 deletions(-) delete mode 100644 +eui/ConditionPanel.m delete mode 100644 +eui/FieldPanel.m delete mode 100644 +eui/ParamEditor_old.m diff --git a/+eui/ConditionPanel.m b/+eui/ConditionPanel.m deleted file mode 100644 index eacd317f..00000000 --- a/+eui/ConditionPanel.m +++ /dev/null @@ -1,254 +0,0 @@ -classdef ConditionPanel < handle - %UNTITLED Summary of this class goes here - % Detailed explanation goes here - % TODO Document - % TODO Add sort by column - % TODO Add set condition idx - % TODO Use tags for menu items - - properties - ConditionTable - MinWidth = 80 -% MaxWidth = 140 -% Margin = 4 - UIPanel - ButtonPanel - ContextMenus - end - - properties %(Access = protected) - ParamEditor - Listener - NewConditionButton - DeleteConditionButton - MakeGlobalButton - SetValuesButton - SelectedCells %[row, column;...] of each selected cell - end - - methods - function obj = ConditionPanel(f, ParamEditor, varargin) - obj.ParamEditor = ParamEditor; - obj.UIPanel = uix.VBox('Parent', f, 'BackgroundColor', 'white'); - % Create a child menu for the uiContextMenus - c = uicontextmenu; - obj.UIPanel.UIContextMenu = c; - obj.ContextMenus = uimenu(c, 'Label', 'Make Global', 'MenuSelectedFcn', @(~,~)obj.makeGlobal); - fcn = @(s,~)obj.ParamEditor.setRandomized(~strcmp(s.Checked, 'on')); - obj.ContextMenus(2) = uimenu(c, 'Label', 'Randomize conditions', ... - 'MenuSelectedFcn', fcn, 'Checked', 'on', 'Tag', 'randomize button'); - obj.ContextMenus(3) = uimenu(c, 'Label', 'Sort by selected column', ... - 'MenuSelectedFcn', @(~,~)disp('feature not yet implemented'), 'Tag', 'sort by'); - % Create condition table - p = uix.Panel('Parent', obj.UIPanel); - obj.ConditionTable = uitable('Parent', p,... - 'FontName', 'Consolas',... - 'RowName', [],... - 'RearrangeableColumns', true,... - 'Units', 'normalized',... - 'Position',[0 0 1 1],... - 'UIContextMenu', c,... - 'CellEditCallback', @obj.onEdit,... - 'CellSelectionCallback', @obj.onSelect); - % Create button panel to hold condition control buttons - obj.ButtonPanel = uix.HBox('Parent', obj.UIPanel, ... - 'BackgroundColor', 'white'); - % Create callback so that width of button panel is slave to width of - % conditional UIPanel -% b = obj.ButtonPanel; -% fcn = @(s)set(obj.ButtonPanel, 'Position', ... -% [s.Position(1) b.Position(2) s.Position(3) b.Position(4)]); -% obj.Listener = event.listener(obj.UIPanel, 'SizeChanged', @(s,~)fcn(s)); - % Define some common properties - props.BackgroundColor = 'white'; - props.Style = 'pushbutton'; - props.Units = 'normalized'; - props.Parent = obj.ButtonPanel; - % Create out four buttons - obj.NewConditionButton = uicontrol(props,... - 'String', 'New condition',... - ...'Position',[0 0 1/4 1],... - 'TooltipString', 'Add a new condition',... - 'Callback', @(~, ~) obj.newCondition()); - obj.DeleteConditionButton = uicontrol(props,... - 'String', 'Delete condition',... - ...'Position',[1/4 0 1/4 1],... - 'TooltipString', 'Delete the selected condition',... - 'Enable', 'off',... - 'Callback', @(~, ~) obj.deleteSelectedConditions()); - obj.MakeGlobalButton = uicontrol(props,... - 'String', 'Globalise parameter',... - ...'Position',[2/4 0 1/4 1],... - 'TooltipString', sprintf(['Make the selected condition-specific parameter global (i.e. not vary by trial)\n'... - 'This will move it to the global parameters section']),... - 'Enable', 'off',... - 'Callback', @(~, ~) obj.makeGlobal()); - obj.SetValuesButton = uicontrol(props,... - 'String', 'Set values',... - ...'Position',[3/4 0 1/4 1],... - 'TooltipString', 'Set selected values to specified value, range or function',... - 'Enable', 'off',... - 'Callback', @(~, ~) obj.setSelectedValues()); - obj.ButtonPanel.Widths = [-1 -1 -1 -1]; - obj.UIPanel.Heights = [-1 25]; - end - - function onEdit(obj, src, eventData) - disp('updating table cell'); - row = eventData.Indices(1); - col = eventData.Indices(2); - paramName = obj.ConditionTable.ColumnName{col}; - newValue = obj.ParamEditor.update(paramName, eventData.NewData, row); - reformed = obj.ParamEditor.paramValue2Control(newValue); - % If successful update the cell with default formatting - data = get(src, 'Data'); - if iscell(reformed) - % The reformed data type is a cell, this should be a one element - % wrapping cell - if numel(reformed) == 1 - reformed = reformed{1}; - else - error('Cannot handle data reformatted data type'); - end - end - data{row,col} = reformed; - set(src, 'Data', data); - end - - function clear(obj) - set(obj.ConditionTable, 'ColumnName', [], ... - 'Data', [], 'ColumnEditable', false); - end - - function delete(obj) - disp('delete called'); - delete(obj.UIPanel); - end - - function onSelect(obj, ~, eventData) - obj.SelectedCells = eventData.Indices; - if size(eventData.Indices, 1) > 0 - % cells selected, enable buttons - set(obj.MakeGlobalButton, 'Enable', 'on'); - set(obj.DeleteConditionButton, 'Enable', 'on'); - set(obj.SetValuesButton, 'Enable', 'on'); - set(obj.ContextMenus(1), 'Enable', 'on'); - set(obj.ContextMenus(3), 'Enable', 'on'); - else - % nothing selected, disable buttons - set(obj.MakeGlobalButton, 'Enable', 'off'); - set(obj.DeleteConditionButton, 'Enable', 'off'); - set(obj.SetValuesButton, 'Enable', 'off'); - set(obj.ContextMenus(1), 'Enable', 'off'); - set(obj.ContextMenus(3), 'Enable', 'off'); - end - end - - function makeGlobal(obj) - if isempty(obj.SelectedCells) - disp('nothing selected') - return - end - [cols, iu] = unique(obj.SelectedCells(:,2)); - names = obj.ConditionTable.ColumnName(cols); - rows = num2cell(obj.SelectedCells(iu,1)); %get rows of unique selected cols - PE = obj.ParamEditor; - cellfun(@PE.globaliseParamAtCell, names, rows); - end - - function deleteSelectedConditions(obj) - %DELETESELECTEDCONDITIONS Removes the selected conditions from table - % The callback for the 'Delete condition' button. This removes the - % selected conditions from the table and if less than two conditions - % remain, globalizes them. - % TODO: comment function better, index in a clearer fashion - % - % See also EXP.PARAMETERS, GLOBALISESELECTEDPARAMETERS - rows = unique(obj.SelectedCells(:,1)); - names = obj.ConditionTable.ColumnName; - numConditions = size(obj.ConditionTable.Data,2); - % If the number of remaining conditions is 1 or less... - if numConditions-length(rows) <= 1 - remainingIdx = find(all(1:numConditions~=rows,1)); - if isempty(remainingIdx); remainingIdx = 1; end - % change selected cells to be all fields (except numRepeats which - % is assumed to always be the last column) - obj.SelectedCells =[ones(length(names),1)*remainingIdx, (1:length(names))']; - %... globalize them - obj.makeGlobal; - else % Otherwise delete the selected conditions as usual - obj.ParamEditor.Parameters.removeConditions(rows); %FIXME: Should be in ParamEditor - end - % Refresh the table of conditions FIXME: Should be in ParamEditor - obj.ParamEditor.fillConditionTable(); - end - - function setSelectedValues(obj) % Set multiple fields in conditional table - disp('updating table cells'); - cols = obj.SelectedCells(:,2); % selected columns - uCol = unique(obj.SelectedCells(:,2)); - rows = obj.SelectedCells(:,1); % selected rows - % get current values of selected cells - currVals = arrayfun(@(u)obj.ConditionTable.Data(rows(cols==u),u), uCol, 'UniformOutput', 0); - names = obj.ConditionTable.ColumnName(uCol); % selected column names - promt = cellfun(@(a,b) [a ' (' num2str(sum(cols==b)) ')'],... - names, num2cell(uCol), 'UniformOutput', 0); % names of columns & num selected rows - defaultans = cellfun(@(c) c(1), currVals); - answer = inputdlg(promt,'Set values', 1, cellflat(defaultans)); % prompt for input - if isempty(answer) % if user presses cancel - return - end - % set values for each column - cellfun(@(a,b,c) setNewVals(a,b,c), answer, currVals, names, 'UniformOutput', 0); - function newVals = setNewVals(userIn, currVals, paramName) - % check array orientation - currVals = iff(size(currVals,1)>size(currVals,2),currVals',currVals); - if strStartsWith(userIn,'@') % anon function - func_h = str2func(userIn); - % apply function to each cell - currVals = cellfun(@str2double,currVals, 'UniformOutput', 0); % convert from char - newVals = cellfun(func_h, currVals, 'UniformOutput', 0); - elseif any(userIn==':') % array syntax - arr = eval(userIn); - newVals = num2cell(arr); % convert to cell array - elseif any(userIn==','|userIn==';') % 2D arrays - C = strsplit(userIn, ';'); - newVals = cellfun(@(c)textscan(c, '%f',... - 'ReturnOnError', false,... - 'delimiter', {' ', ','}, 'MultipleDelimsAsOne', 1),... - C); - else % single value to copy across all cells - userIn = str2double(userIn); - newVals = num2cell(ones(size(currVals))*userIn); - end - - if length(newVals)>length(currVals) % too many new values - newVals = newVals(1:length(currVals)); % truncate new array - elseif length(newVals) 5; w = 0.5; else; w = 0.1 * w; end -% obj.UI(2).Position = [1-w 0 w 1]; -% obj.UI(1).Position = [0 0 1-w 1]; - - %%% general coordinates - pos = getpixelposition(obj.UIPanel); - borderwidth = obj.Margin; - bounds = [pos(3) pos(4)] - 2*borderwidth; - n = numel(obj.Labels); - vspace = obj.RowSpacing; - hspace = obj.ColSpacing; - rowHeight = obj.MinRowHeight + 2*vspace; - rowsPerCol = floor(bounds(2)/rowHeight); - cols = ceil((1:n)/rowsPerCol)'; - ncols = cols(end); - rows = mod(0:n - 1, rowsPerCol)' + 1; - labelColWidth = max(obj.LabelWidths) + 2*hspace; - ctrlWidthAvail = bounds(1)/ncols - labelColWidth; - ctrlColWidth = max(obj.MinCtrlWidth, min(ctrlWidthAvail, obj.MaxCtrlWidth)); - fullColWidth = labelColWidth + ctrlColWidth; - - %%% coordinates of labels - by = bounds(2) - rows*rowHeight + vspace + 1 + borderwidth; - labelPos = [vspace + (cols - 1)*fullColWidth + 1 + borderwidth... - by... - obj.LabelWidths... - repmat(rowHeight - 2*vspace, n, 1)]; - - %%% coordinates of edits - editPos = [labelColWidth + hspace + (cols - 1)*fullColWidth + 1 + borderwidth ... - by... - repmat(ctrlColWidth - 2*hspace, n, 1)... - repmat(rowHeight - 2*vspace, n, 1)]; - set(obj.Labels, {'Position'}, num2cell(labelPos, 2)); - set(obj.Controls, {'Position'}, num2cell(editPos, 2)); - - end - end - -end - diff --git a/+eui/MControl.m b/+eui/MControl.m index 69c8628d..5b25075d 100644 --- a/+eui/MControl.m +++ b/+eui/MControl.m @@ -247,10 +247,10 @@ function saveParamProfile(obj) % Called by 'Save...' button press, save a new pa end function loadParamProfile(obj, profile) - set(obj.ParamProfileLabel, 'String', 'loading...', 'ForegroundColor', [1 0 0]); % Red 'Loading...' while new set loads if ~isempty(obj.ParamEditor) - % Clear existing parameters control - clear(obj.ParamEditor) + %delete existing parameters control + delete(obj.ParamEditor); + set(obj.ParamProfileLabel, 'String', 'loading...', 'ForegroundColor', [1 0 0]); % Red 'Loading...' while new set loads end factory = obj.NewExpFactory; % Find which 'world' we are in @@ -305,18 +305,12 @@ function loadParamProfile(obj, profile) paramStruct = rmfield(paramStruct, 'services'); end obj.Parameters.Struct = paramStruct; - if isempty(paramStruct); return; end - % Now parameters are loaded, pass to ParamEditor for display, etc. - if isempty(obj.ParamEditor) - panel = uipanel('Parent', obj.ParamPanel, 'Position', [0 0 1 1]); -% panel = uiextras.Panel('Parent', obj.ParamPanel); - obj.ParamEditor = eui.ParamEditor(obj.Parameters, panel); % Build parameter list in Global panel by calling eui.ParamEditor - else - obj.ParamEditor.buildUI(obj.Parameters); - end - obj.ParamEditor.addlistener('Changed', @(src,~) obj.paramChanged); - if strcmp(obj.RemoteRigs.Selected.Status, 'idle') - set(obj.BeginExpButton, 'Enable', 'on') % Re-enable start button + if ~isempty(paramStruct) % Now parameters are loaded, pass to ParamEditor for display, etc. + obj.ParamEditor = eui.ParamEditor(obj.Parameters, obj.ParamPanel); % Build parameter list in Global panel by calling eui.ParamEditor + obj.ParamEditor.addlistener('Changed', @(src,~) obj.paramChanged); + if strcmp(obj.RemoteRigs.Selected.Status, 'idle') + set(obj.BeginExpButton, 'Enable', 'on') % Re-enable start button + end end end diff --git a/+eui/ParamEditor.m b/+eui/ParamEditor.m index 4c375b7e..75aca882 100644 --- a/+eui/ParamEditor.m +++ b/+eui/ParamEditor.m @@ -1,130 +1,155 @@ classdef ParamEditor < handle - %UNTITLED2 Summary of this class goes here - % Detailed explanation goes here + %EUI.PARAMEDITOR UI control for configuring experiment parameters + % TODO. See also EXP.PARAMETERS. + % + % Part of Rigbox + + % 2012-11 CB created + % 2017-03 MW/NS Made global panel scrollable & improved performance of + % buildGlobalUI. + % 2017-03 MW Added set values button properties + GlobalVSpacing = 20 Parameters end - properties %(Access = private) - UIPanel - GlobalUI - ConditionalUI - Parent - Listener - end - properties (Dependent) Enable end + properties (Access = private) + Root + GlobalGrid + ConditionTable + TableColumnParamNames = {} + NewConditionButton + DeleteConditionButton + MakeGlobalButton + SetValuesButton + SelectedCells %[row, column;...] of each selected cell + GlobalControls + end + events Changed end methods - function obj = ParamEditor(pars, f) - if nargin == 0; pars = []; end - if nargin < 2 - f = figure('Name', 'Parameters', 'NumberTitle', 'off',... - 'Toolbar', 'none', 'Menubar', 'none', 'DeleteFcn', @(~,~)obj.delete); - obj.Listener = event.listener(f, 'SizeChanged', @(~,~)obj.onResize); + function obj = ParamEditor(params, parent) + if nargin < 2 % Can call this function to display parameters is new window + parent = figure('Name', 'Parameters', 'NumberTitle', 'off',... + 'Toolbar', 'none', 'Menubar', 'none'); end - obj.Parent = f; - obj.UIPanel = uix.HBox('Parent', f); - obj.GlobalUI = eui.FieldPanel(obj.UIPanel, obj); - obj.ConditionalUI = eui.ConditionPanel(obj.UIPanel, obj); - obj.buildUI(pars); + obj.Parameters = params; + obj.build(parent); end function delete(obj) - delete(obj.GlobalUI); - delete(obj.ConditionalUI); - end - - function set.Enable(obj, value) - cUI = obj.ConditionalUI; - fig = obj.Parent; - if value == true - arrayfun(@(prop) set(prop, 'Enable', 'on'), findobj(fig,'Enable','off')); - if isempty(cUI.SelectedCells) - set(cUI.MakeGlobalButton, 'Enable', 'off'); - set(cUI.DeleteConditionButton, 'Enable', 'off'); - set(cUI.SetValuesButton, 'Enable', 'off'); - end - obj.Enable = true; - else - arrayfun(@(prop) set(prop, 'Enable', 'off'), findobj(fig,'Enable','on')); - obj.Enable = false; + disp('ParamEditor destructor called'); + if obj.Root.isvalid + obj.Root.delete(); end end - function clear(obj) - clear(obj.GlobalUI); - clear(obj.ConditionalUI); + function value = get.Enable(obj) + value = obj.Root.Enable; end - function buildUI(obj, pars) - obj.Parameters = pars; - obj.clear() - c = obj.GlobalUI; - names = pars.GlobalNames; - for nm = names' - if strcmp(nm, 'randomiseConditions'); continue; end - if islogical(pars.Struct.(nm{:})) % If parameter is logical, make checkbox - ctrl = uicontrol('Parent', c.UIPanel, 'Style', 'checkbox', ... - 'Value', pars.Struct.(nm{:}), 'BackgroundColor', 'white'); - addField(c, nm{:}, ctrl); - else - [~, ctrl] = addField(c, nm{:}); - ctrl.String = obj.paramValue2Control(pars.Struct.(nm{:})); - end - end + function set.Enable(obj, value) + obj.Root.Enable = value; + end + end + + methods %(Access = protected) + function build(obj, parent) % Build parameters panel + obj.Root = uiextras.HBox('Parent', parent, 'Padding', 5, 'Spacing', 5); % Add horizontal container for Global and Conditional panels +% globalPanel = uiextras.Panel('Parent', obj.Root,... % Make 'Global' parameters panel +% 'Title', 'Global', 'Padding', 5); + globPanel = uiextras.Panel('Parent', obj.Root,... % Make 'Global' parameters panel + 'Title', 'Global', 'Padding', 5); + globalPanel = uix.ScrollingPanel('Parent', globPanel,... % Make 'Global' scroll panel + 'Padding', 5); + + obj.GlobalGrid = uiextras.Grid('Parent', globalPanel, 'Padding', 4); % Make grid for parameter fields + obj.buildGlobalUI; % Populate Global panel + globalPanel.Heights = sum(obj.GlobalGrid.RowSizes)+45; + + conditionPanel = uiextras.Panel('Parent', obj.Root,... + 'Title', 'Conditional', 'Padding', 5); % Make 'Conditional' parameters panel + conditionVBox = uiextras.VBox('Parent', conditionPanel); + obj.ConditionTable = uitable('Parent', conditionVBox,... + 'FontName', 'Consolas',... + 'RowName', [],... + 'CellEditCallback', @obj.cellEditCallback,... + 'CellSelectionCallback', @obj.cellSelectionCallback); obj.fillConditionTable(); - %%% Special parameters - if ismember('randomiseConditions', obj.Parameters.Names) && ~pars.Struct.randomiseConditions - obj.ConditionalUI.ConditionTable.RowName = 'numbered'; - set(obj.ConditionalUI.ContextMenus(2), 'Checked', 'off'); - end - obj.GlobalUI.onResize(); + conditionButtonBox = uiextras.HBox('Parent', conditionVBox); + conditionVBox.Sizes = [-1 25]; + obj.NewConditionButton = uicontrol('Parent', conditionButtonBox,... + 'Style', 'pushbutton',... + 'String', 'New condition',... + 'TooltipString', 'Add a new condition',... + 'Callback', @(~, ~) obj.newCondition()); + obj.DeleteConditionButton = uicontrol('Parent', conditionButtonBox,... + 'Style', 'pushbutton',... + 'String', 'Delete condition',... + 'TooltipString', 'Delete the selected condition',... + 'Enable', 'off',... + 'Callback', @(~, ~) obj.deleteSelectedConditions()); + obj.MakeGlobalButton = uicontrol('Parent', conditionButtonBox,... + 'Style', 'pushbutton',... + 'String', 'Globalise parameter',... + 'TooltipString', sprintf(['Make the selected condition-specific parameter global (i.e. not vary by trial)\n'... + 'This will move it to the global parameters section']),... + 'Enable', 'off',... + 'Callback', @(~, ~) obj.globaliseSelectedParameters()); + obj.SetValuesButton = uicontrol('Parent', conditionButtonBox,... + 'Style', 'pushbutton',... + 'String', 'Set values',... + 'TooltipString', 'Set selected values to specified value, range or function',... + 'Enable', 'off',... + 'Callback', @(~, ~) obj.setSelectedValues()); + + obj.Root.Sizes = [sum(obj.GlobalGrid.ColumnSizes) + 32, -1]; end - function setRandomized(obj, value) - % If randomiseConditions doesn't exist and new value is false, add - % the parameter and set it to false - if ~ismember('randomiseConditions', obj.Parameters.Names) && value == false - description = 'Whether to randomise the conditional paramters or present them in order'; - obj.Parameters.set('randomiseConditions', false, description, 'logical') - elseif ismember('randomiseConditions', obj.Parameters.Names) - obj.update('randomiseConditions', logical(value)); - end - menu = obj.ConditionalUI.ContextMenus(2); - if value == false - obj.ConditionalUI.ConditionTable.RowName = 'numbered'; - menu.Checked = 'off'; - else - obj.ConditionalUI.ConditionTable.RowName = []; - menu.Checked = 'on'; + function buildGlobalUI(obj) % Function to essemble global parameters + globalParamNames = fieldnames(obj.Parameters.assortForExperiment); % assortForExperiment divides params into global and trial-specific parameter structures + obj.GlobalControls = gobjects(length(globalParamNames),3); % Initialize object array (faster than assigning to end of array which results in two calls to constructor) + for i=1:length(globalParamNames) % using for loop (sorry Chris!) to populate object array 2017-02-14 MW + [obj.GlobalControls(i,1), obj.GlobalControls(i,2), obj.GlobalControls(i,3)]... % [editors, labels, buttons] + = obj.addParamUI(globalParamNames{i}); end + % Above code replaces the following as after 2014a, MATLAB doesn't no + % longer uses numrical handles but instead uses object arrays +% [editors, labels, buttons] = cellfun(... +% @(n) obj.addParamUI(n), fieldnames(globalParams), 'UniformOutput', false); +% editors = cell2mat(editors); +% labels = cell2mat(labels); +% buttons = cell2mat(buttons); +% obj.GlobalControls = [labels, editors, buttons]; +% obj.GlobalGrid.Children = obj.GlobalControls(:); + +% obj.GlobalGrid.Children = +% blah = cat(1,obj.GlobalControls(:,1),obj.GlobalControls(:,2),obj.GlobalControls(:,3)); +% Doesn't work for some reason - MW 2017-02-15 + + child_handles = allchild(obj.GlobalGrid); % Get child handles for GlobalGrid + child_handles = [child_handles(end-1:-3:1); child_handles(end:-3:1); child_handles(end-2:-3:1)]; % Reorder them so all labels come first, then ctrls, then buttons +% child_handles = [child_handles(2:3:end); child_handles(3:3:end); child_handles(1:3:end)]; % Reorder them so all labels come first, then ctrls, then buttons + obj.GlobalGrid.Contents = child_handles; % Set children to new order + % uistack + + obj.GlobalGrid.ColumnSizes = [180, 200, 40]; % Set column sizes + obj.GlobalGrid.Spacing = 1; + obj.GlobalGrid.RowSizes = repmat(obj.GlobalVSpacing, 1, size(obj.GlobalControls, 1)); end - function fillConditionTable(obj) - % Build the condition table - titles = obj.Parameters.TrialSpecificNames; - [~, trialParams] = obj.Parameters.assortForExperiment; - if isempty(titles) - obj.ConditionalUI.ButtonPanel.Visible = 'off'; - obj.ConditionalUI.UIPanel.Visible = 'off'; - obj.GlobalUI.UIPanel.Position(3) = 1; - else - obj.ConditionalUI.ButtonPanel.Visible = 'on'; - obj.ConditionalUI.UIPanel.Visible = 'on'; - data = reshape(struct2cell(trialParams), numel(titles), [])'; - data = mapToCell(@(e) obj.paramValue2Control(e), data); - set(obj.ConditionalUI.ConditionTable, 'ColumnName', titles, 'Data', data,... - 'ColumnEditable', true(1, numel(titles))); - end - end +% function swapConditions(obj, idx1, idx2) % Function started, never +% finished - MW 2017-02-15 +% % params = obj.Parameters.trial +% end function addEmptyConditionToParam(obj, name) assert(obj.Parameters.isTrialSpecific(name),... @@ -158,134 +183,217 @@ function addEmptyConditionToParam(obj, name) obj.Parameters.Struct.(name) = cat(2, obj.Parameters.Struct.(name), newValue); end - function newValue = update(obj, name, value, row) - % FIXME change name to updateGlobal - if nargin < 4; row = 1; end - currValue = obj.Parameters.Struct.(name)(:,row); - if iscell(currValue) - % cell holders are allowed to be different types of value - newValue = obj.controlValue2Param(currValue{1}, value, true); - obj.Parameters.Struct.(name){:,row} = newValue; + function cellSelectionCallback(obj, src, eventData) + obj.SelectedCells = eventData.Indices; + if size(eventData.Indices, 1) > 0 + %cells selected, enable buttons + set(obj.MakeGlobalButton, 'Enable', 'on'); + set(obj.DeleteConditionButton, 'Enable', 'on'); + set(obj.SetValuesButton, 'Enable', 'on'); else - newValue = obj.controlValue2Param(currValue, value); - obj.Parameters.Struct.(name)(:,row) = newValue; + %nothing selected, disable buttons + set(obj.MakeGlobalButton, 'Enable', 'off'); + set(obj.DeleteConditionButton, 'Enable', 'off'); + set(obj.SetValuesButton, 'Enable', 'off'); end - notify(obj, 'Changed'); end - function globaliseParamAtCell(obj, name, row) - % Make parameter 'name' a global parameter and set it's value to be - % that of the specified row. + function newCondition(obj) + disp('adding new condition row'); + cellfun(@obj.addEmptyConditionToParam, obj.Parameters.TrialSpecificNames); + obj.fillConditionTable(); + end + + function deleteSelectedConditions(obj) + %DELETESELECTEDCONDITIONS Removes the selected conditions from table + % The callback for the 'Delete condition' button. This removes the + % selected conditions from the table and if less than two conditions + % remain, globalizes them. + % TODO: comment function better, index in a clearer fashion % - % See also EXP.PARAMETERS/MAKEGLOBAL, UI.CONDITIONPANEL/MAKEGLOBAL + % See also EXP.PARAMETERS, GLOBALISESELECTEDPARAMETERS + rows = unique(obj.SelectedCells(:,1)); + % If the number of remaining conditions is 1 or less... + names = obj.Parameters.TrialSpecificNames; + numConditions = size(obj.Parameters.Struct.(names{1}),2); + if numConditions-length(rows) <= 1 + remainingIdx = find(all(1:numConditions~=rows,1)); + if isempty(remainingIdx); remainingIdx = 1; end + % change selected cells to be all fields (except numRepeats which + % is assumed to always be the last column) + obj.SelectedCells =[ones(length(names)-1,1)*remainingIdx, (1:length(names)-1)']; + %... globalize them + obj.globaliseSelectedParameters; + obj.Parameters.removeConditions(rows) +% for i = 1:numel(names) +% newValue = iff(any(remainingIdx), obj.Struct.(names{i})(:,remainingIdx), obj.Struct.(names{i})(1)); +% % If the parameter is Num repeats, set the value +% if strcmp(names{i}, 'numRepeats') +% obj.Struct.(names{i}) = newValue; +% else +% obj.makeGlobal(names{i}, newValue); +% end +% end + else % Otherwise delete the selected conditions as usual + obj.Parameters.removeConditions(rows); + end + obj.fillConditionTable(); %refresh the table of conditions + end + + function globaliseSelectedParameters(obj) + [cols, iu] = unique(obj.SelectedCells(:,2)); + names = obj.TableColumnParamNames(cols); + rows = obj.SelectedCells(iu,1); %get rows of unique selected cols + arrayfun(@obj.globaliseParamAtCell, rows, cols); + obj.fillConditionTable(); %refresh the table of conditions + %now add global controls for parameters + newGlobals = gobjects(length(names),3); % Initialize object array (faster than assigning to end of array which results in two calls to constructor) + for i=length(names):-1:1 % using for loop (sorry Chris!) to initialize and populate object array 2017-02-15 MW + [newGlobals(i,1), newGlobals(i,2), newGlobals(i,3)]... % [editors, labels, buttons] + = obj.addParamUI(names{i}); + end + +% [editors, labels, buttons] = arrayfun(@obj.addParamUI, names); % +% 2017-02-15 MW can no longer use arrayfun with object outputs + idx = size(obj.GlobalControls, 1); % Calculate number of current Global params + new = numel(newGlobals); + obj.GlobalControls = [obj.GlobalControls; newGlobals]; % Add new globals to object + ggHandles = obj.GlobalGrid.Contents; + ggHandles = [ggHandles(1:idx); ggHandles((end-new+2):3:end);... + ggHandles(idx+1:idx*2); ggHandles((end-new+1):3:end);... + ggHandles(idx*2+1:idx*3); ggHandles((end-new+3):3:end)]; % Reorder them so all labels come first, then ctrls, then buttons + obj.GlobalGrid.Contents = ggHandles; % Set children to new order + + % Reset sizes + obj.GlobalGrid.RowSizes = repmat(obj.GlobalVSpacing, 1, size(obj.GlobalControls, 1)); + set(get(obj.GlobalGrid, 'Parent'),... + 'Heights', sum(obj.GlobalGrid.RowSizes)+45); % Reset height of globalPanel + obj.GlobalGrid.ColumnSizes = [180, 200, 40]; + obj.GlobalGrid.Spacing = 1; + end + + function globaliseParamAtCell(obj, row, col) + name = obj.TableColumnParamNames{col}; value = obj.Parameters.Struct.(name)(:,row); obj.Parameters.makeGlobal(name, value); - % Refresh the table of conditions - obj.fillConditionTable; - % Add new global parameter to field panel - if islogical(value) % If parameter is logical, make checkbox - ctrl = uicontrol('Parent', obj.GlobalUI.UIPanel, 'Style', 'checkbox', ... - 'Value', value, 'BackgroundColor', 'white'); - addField(obj.GlobalUI, name, ctrl); - else - [~, ctrl] = addField(obj.GlobalUI, name); - ctrl.String = obj.paramValue2Control(value); - end - obj.GlobalUI.onResize(); - obj.notify('Changed'); end - - function onResize(obj) - %%% resize condition table - notify(obj.ConditionalUI.ButtonPanel, 'SizeChanged'); - cUI = obj.ConditionalUI.UIPanel; - gUI = obj.GlobalUI.UIPanel; - - pos = obj.GlobalUI.Controls(end).Position; - colExtent = pos(1) + pos(3) + obj.GlobalUI.Margin; - colWidth = pos(3) + obj.GlobalUI.Margin + obj.GlobalUI.ColSpacing; % FIXME: inaccurate - pos = getpixelposition(gUI); - gUIExtent = pos(3); - pos = getpixelposition(cUI); - cUIExtent = pos(3); - - extent = get(obj.ConditionalUI.ConditionTable, 'Extent'); - panelWidth = cUI.Position(3); - if colExtent > gUIExtent && cUIExtent > obj.ConditionalUI.MinWidth - % If global UI controls are cut off and there is no dead space in - % the table but the minimum table width hasn't been reached, reduce - % the conditional UI width: table has scroll bar and global panel - % does not - % FIXME calculate how much space required for min control width -% obj.GlobalUI.MinCtrlWidth - % Calculate conditional UI width in normalized units - requiredWidth = (cUI.Position(3) / cUIExtent) * (colExtent - gUIExtent); - minConditionalWidth = (cUI.Position(3) / cUIExtent) * obj.ConditionalUI.MinWidth; - if requiredWidth < minConditionalWidth - % If the required width is smaller that the minimum table width, - % use minimum table width - cUI.Position(3) = minConditionalWidth; - else % Otherwise use this width - cUI.Position(3) = requiredWidth; + + function setSelectedValues(obj) % Set multiple fields in conditional table + disp('updating table cells'); + cols = obj.SelectedCells(:,2); % selected columns + uCol = unique(obj.SelectedCells(:,2)); + rows = obj.SelectedCells(:,1); % selected rows + % get current values of selected cells + currVals = arrayfun(@(u)obj.ConditionTable.Data(rows(cols==u),u), uCol, 'UniformOutput', 0); + names = obj.TableColumnParamNames(uCol); % selected column names + promt = cellfun(@(a,b) [a ' (' num2str(sum(cols==b)) ')'],... + names, num2cell(uCol), 'UniformOutput', 0); % names of columns & num selected rows + defaultans = cellfun(@(c) c(1), currVals); + answer = inputdlg(promt,'Set values', 1, cellflat(defaultans)); % prompt for input + if isempty(answer) % if user presses cancel + return + end + % set values for each column + cellfun(@(a,b,c) setNewVals(a,b,c), answer, currVals, names, 'UniformOutput', 0); + function newVals = setNewVals(userIn, currVals, paramName) + % check array orientation + currVals = iff(size(currVals,1)>size(currVals,2),currVals',currVals); + if strStartsWith(userIn,'@') % anon function + func_h = str2func(userIn); + % apply function to each cell + currVals = cellfun(@str2double,currVals, 'UniformOutput', 0); % convert from char + newVals = cellfun(func_h, currVals, 'UniformOutput', 0); + elseif any(userIn==':') % array syntax + arr = eval(userIn); + newVals = num2cell(arr); % convert to cell array + elseif any(userIn==','|userIn==';') % 2D arrays + C = strsplit(userIn, ';'); + newVals = cellfun(@(c)textscan(c, '%f',... + 'ReturnOnError', false,... + 'delimiter', {' ', ','}, 'MultipleDelimsAsOne', 1),... + C); + else % single value to copy across all cells + userIn = str2double(userIn); + newVals = num2cell(ones(size(currVals))*userIn); + end + + if length(newVals)>length(currVals) % too many new values + newVals = newVals(1:length(currVals)); % truncate new array + elseif length(newVals)= 1 && colExtent < gUIExtent - % If the table space is cut off and there is dead space in the - % global UI panel, reduce the global UI panel - % If the extra space is minimum, return - if floor(gUIExtent - colExtent) <= 2; return; end - deadspace = gUIExtent - colExtent; % Spece between panels in pixels - gUI.Position(3) = (gUI.Position(3) / gUIExtent) * (gUIExtent - deadspace); - cUI.Position(3) = 1-gUI.Position(3); - cUI.Position(1) = gUI.Position(3); + notify(obj, 'Changed'); + end + + function cellEditCallback(obj, src, eventData) + disp('updating table cell'); + row = eventData.Indices(1); + col = eventData.Indices(2); + paramName = obj.TableColumnParamNames{col}; + currValue = obj.Parameters.Struct.(paramName)(:,row); + if iscell(currValue) + % cell holders are allowed to be different types of value + newParam = obj.controlValue2Param(currValue{1}, eventData.NewData, true); + obj.Parameters.Struct.(paramName){:,row} = newParam; else - % Compromise by having both panels take up half the figure -% [cUI.Position([1,3]), gUI.Position(3)] = deal(0.5); + newParam = obj.controlValue2Param(currValue, eventData.NewData); + obj.Parameters.Struct.(paramName)(:,row) = newParam; end - notify(obj.ConditionalUI.ButtonPanel, 'SizeChanged'); + % if successful update the cell with default formatting + data = get(src, 'Data'); + reformed = obj.paramValue2Control(newParam); + if iscell(reformed) + % the reformed data type is a cell, this should be a one element + % wrapping cell + if numel(reformed) == 1 + reformed = reformed{1}; + else + error('Cannot handle data reformatted data type'); + end + end + data{row,col} = reformed; + set(src, 'Data', data); + %notify listeners of change + notify(obj, 'Changed'); end - end - - methods (Static) - function data = paramValue2Control(data) - % convert from parameter value to control value, i.e. a value class - % that can be easily displayed and edited by the user. Everything - % except logicals are converted to charecter arrays. - switch class(data) - case 'function_handle' - % convert a function handle to it's string name - data = func2str(data); - case 'logical' - data = data ~= 0; % If logical do nothing, basically. - case 'string' - data = char(data); % Strings not allowed in condition table data - otherwise - if isnumeric(data) - % format numeric types as string number list - strlist = mapToCell(@num2str, data); - data = strJoin(strlist, ', '); - elseif iscellstr(data) - data = strJoin(data, ', '); - end + + function updateGlobal(obj, param, src) + currParamValue = obj.Parameters.Struct.(param); + switch get(src, 'style') + case 'checkbox' + newValue = logical(get(src, 'value')); + obj.Parameters.Struct.(param) = newValue; + case 'edit' + newValue = obj.controlValue2Param(currParamValue, get(src, 'string')); + obj.Parameters.Struct.(param) = newValue; + % if successful update the control with default formatting and + % modified colour + set(src, 'String', obj.paramValue2Control(newValue),... + 'ForegroundColor', [1 0 0]); %red indicating it has changed + %notify listeners of change + notify(obj, 'Changed'); end - % all other data types stay as they are + end + + function [data, paramNames, titles] = tableData(obj) + [~, trialParams] = obj.Parameters.assortForExperiment; + paramNames = fieldnames(trialParams); + titles = obj.Parameters.title(paramNames); + data = reshape(struct2cell(trialParams), numel(paramNames), [])'; + data = mapToCell(@(e) obj.paramValue2Control(e), data); end - function data = controlValue2Param(currParam, data, allowTypeChange) + function data = controlValue2Param(obj, currParam, data, allowTypeChange) % Convert the values displayed in the UI ('control values') to % parameter values. String representations of numrical arrays and % functions are converted back to their 'native' classes. @@ -324,7 +432,111 @@ function onResize(obj) end end end + + function data = paramValue2Control(obj, data) + % convert from parameter value to control value, i.e. a value class + % that can be easily displayed and edited by the user. Everything + % except logicals are converted to charecter arrays. + switch class(data) + case 'function_handle' + % convert a function handle to it's string name + data = func2str(data); + case 'logical' + data = data ~= 0; % If logical do nothing, basically. + case 'string' + data = char(data); % Strings not allowed in condition table data + otherwise + if isnumeric(data) + % format numeric types as string number list + strlist = mapToCell(@num2str, data); + data = strJoin(strlist, ', '); + elseif iscellstr(data) + data = strJoin(data, ', '); + end + end + % all other data types stay as they are + end + + function fillConditionTable(obj) + [data, params, titles] = obj.tableData; + set(obj.ConditionTable, 'ColumnName', titles, 'Data', data,... + 'ColumnEditable', true(1, numel(titles))); + obj.TableColumnParamNames = params; + end + + function makeTrialSpecific(obj, paramName, ctrls) + [uirow, ~] = find(obj.GlobalControls == ctrls{1}); + assert(numel(uirow) == 1, 'Unexpected number of matching global controls'); + cellfun(@(c) delete(c), ctrls); + obj.GlobalControls(uirow,:) = []; + obj.GlobalGrid.RowSizes(uirow) = []; + obj.Parameters.makeTrialSpecific(paramName); + obj.fillConditionTable(); + set(get(obj.GlobalGrid, 'Parent'),... + 'Heights', sum(obj.GlobalGrid.RowSizes)+45); % Reset height of globalPanel + end + function [ctrl, label, buttons] = addParamUI(obj, name) % Adds ui element for each parameter + parent = obj.GlobalGrid; % Made by build function above + ctrl = []; + label = []; + buttons = []; + if iscell(name) % 2017-02-14 MW function now called with arrayFun (instead of cellFun) + name = name{1,1}; + end + value = obj.paramValue2Control(obj.Parameters.Struct.(name)); % convert from parameter value to control value (everything but logical values become strings) + title = obj.Parameters.title(name); + description = obj.Parameters.description(name); + + if islogical(value) % If parameter is logical, make checkbox + for i = 1:length(value) + ctrl(end+1) = uicontrol('Parent', parent,... + 'Style', 'checkbox',... + 'TooltipString', description,... + 'Value', value(i),... % Added 2017-02-15 MW set checkbox to what ever the parameter value is + 'Callback', @(src, e) obj.updateGlobal(name, src)); + end + elseif ischar(value) + ctrl = uicontrol('Parent', parent,... + 'BackgroundColor', [1 1 1],... + 'Style', 'edit',... + 'String', value,... + 'TooltipString', description,... + 'UserData', name,... % save the name of the parameter in userdata + 'HorizontalAlignment', 'left',... + 'Callback', @(src, e) obj.updateGlobal(name, src)); +% elseif iscellstr(value) +% lines = mkStr(value, [], sprintf('\n'), []); +% ctrl = uicontrol('Parent', parent,... +% 'BackgroundColor', [1 1 1],... +% 'Style', 'edit',... +% 'Max', 2,... %make it multiline +% 'String', lines,... +% 'TooltipString', description,... +% 'HorizontalAlignment', 'left',... +% 'UserData', name,... % save the name of the parameter in userdata +% 'Callback', @(src, e) obj.updateGlobal(name, src)); + end + + if ~isempty(ctrl) % If control box is made, add label and conditional button + label = uicontrol('Parent', parent,... + 'Style', 'text', 'String', title, 'HorizontalAlignment', 'left',... + 'TooltipString', description); % Why not use bui.label? MW 2017-02-15 + bbox = uiextras.HBox('Parent', parent); % Make HBox for button + % UIContainer no longer present in GUILayoutToolbox, it used to + % call uipanel with the following args: + % 'Units', 'Normalized'; 'BorderType', 'none') +% buttons = bbox.UIContainer; + buttons = uicontrol('Parent', bbox, 'Style', 'pushbutton',... % Make 'conditional parameter' button + 'String', '[...]',... + 'TooltipString', sprintf(['Make this a condition parameter (i.e. vary by trial).\n'... + 'This will move it to the trial conditions table.']),... + 'FontSize', 7,... + 'Callback', @(~,~) obj.makeTrialSpecific(name, {ctrl, label, bbox})); + bbox.Sizes = 29; % Resize button height to 29px + end + end end + end diff --git a/+eui/ParamEditor_old.m b/+eui/ParamEditor_old.m deleted file mode 100644 index 05855aef..00000000 --- a/+eui/ParamEditor_old.m +++ /dev/null @@ -1,516 +0,0 @@ -classdef ParamEditor < handle - %EUI.PARAMEDITOR UI control for configuring experiment parameters - % TODO. See also EXP.PARAMETERS. - % - % Part of Rigbox - - % 2012-11 CB created - % 2017-03 MW/NS Made global panel scrollable & improved performance of - % buildGlobalUI. - % 2017-03 MW Added set values button - - properties - GlobalVSpacing = 20 - Parameters - end - - properties (Dependent) - Enable - end - - properties (Access = private) - Root - GlobalGrid - ConditionTable - TableColumnParamNames = {} - NewConditionButton - DeleteConditionButton - MakeGlobalButton - SetValuesButton - SelectedCells %[row, column;...] of each selected cell - GlobalControls - end - - events - Changed - end - - methods - function obj = ParamEditor(params, parent) - if nargin < 2 % Can call this function to display parameters is new window - parent = figure('Name', 'Parameters', 'NumberTitle', 'off',... - 'Toolbar', 'none', 'Menubar', 'none'); - end - obj.Parameters = params; - obj.build(parent); - end - - function delete(obj) - disp('ParamEditor destructor called'); - if obj.Root.isvalid - obj.Root.delete(); - end - end - - function value = get.Enable(obj) - value = obj.Root.Enable; - end - - function set.Enable(obj, value) - obj.Root.Enable = value; - end - end - - methods %(Access = protected) - function build(obj, parent) % Build parameters panel - obj.Root = uiextras.HBox('Parent', parent, 'Padding', 5, 'Spacing', 5); % Add horizontal container for Global and Conditional panels -% globalPanel = uiextras.Panel('Parent', obj.Root,... % Make 'Global' parameters panel -% 'Title', 'Global', 'Padding', 5); - globPanel = uiextras.Panel('Parent', obj.Root,... % Make 'Global' parameters panel - 'Title', 'Global', 'Padding', 5); - globalPanel = uix.ScrollingPanel('Parent', globPanel,... % Make 'Global' scroll panel - 'Padding', 5); - - obj.GlobalGrid = uiextras.Grid('Parent', globalPanel, 'Padding', 4); % Make grid for parameter fields - obj.buildGlobalUI; % Populate Global panel - globalPanel.Heights = sum(obj.GlobalGrid.RowSizes)+45; - - conditionPanel = uiextras.Panel('Parent', obj.Root,... - 'Title', 'Conditional', 'Padding', 5); % Make 'Conditional' parameters panel - conditionVBox = uiextras.VBox('Parent', conditionPanel); - obj.ConditionTable = uitable('Parent', conditionVBox,... - 'FontName', 'Consolas',... - 'RowName', [],... - 'CellEditCallback', @obj.cellEditCallback,... - 'CellSelectionCallback', @obj.cellSelectionCallback); - obj.fillConditionTable(); - conditionButtonBox = uiextras.HBox('Parent', conditionVBox); - conditionVBox.Sizes = [-1 25]; - obj.NewConditionButton = uicontrol('Parent', conditionButtonBox,... - 'Style', 'pushbutton',... - 'String', 'New condition',... - 'TooltipString', 'Add a new condition',... - 'Callback', @(~, ~) obj.newCondition()); - obj.DeleteConditionButton = uicontrol('Parent', conditionButtonBox,... - 'Style', 'pushbutton',... - 'String', 'Delete condition',... - 'TooltipString', 'Delete the selected condition',... - 'Enable', 'off',... - 'Callback', @(~, ~) obj.deleteSelectedConditions()); - obj.MakeGlobalButton = uicontrol('Parent', conditionButtonBox,... - 'Style', 'pushbutton',... - 'String', 'Globalise parameter',... - 'TooltipString', sprintf(['Make the selected condition-specific parameter global (i.e. not vary by trial)\n'... - 'This will move it to the global parameters section']),... - 'Enable', 'off',... - 'Callback', @(~, ~) obj.globaliseSelectedParameters()); - obj.SetValuesButton = uicontrol('Parent', conditionButtonBox,... - 'Style', 'pushbutton',... - 'String', 'Set values',... - 'TooltipString', 'Set selected values to specified value, range or function',... - 'Enable', 'off',... - 'Callback', @(~, ~) obj.setSelectedValues()); - - obj.Root.Sizes = [sum(obj.GlobalGrid.ColumnSizes) + 32, -1]; - end - - function buildGlobalUI(obj) % Function to essemble global parameters - globalParamNames = fieldnames(obj.Parameters.assortForExperiment); % assortForExperiment divides params into global and trial-specific parameter structures - obj.GlobalControls = gobjects(length(globalParamNames),3); % Initialize object array (faster than assigning to end of array which results in two calls to constructor) - for i=1:length(globalParamNames) % using for loop (sorry Chris!) to populate object array 2017-02-14 MW - [obj.GlobalControls(i,1), obj.GlobalControls(i,2), obj.GlobalControls(i,3)]... % [editors, labels, buttons] - = obj.addParamUI(globalParamNames{i}); - end - - child_handles = allchild(obj.GlobalGrid); % Get child handles for GlobalGrid - child_handles = [child_handles(end-1:-3:1); child_handles(end:-3:1); child_handles(end-2:-3:1)]; % Reorder them so all labels come first, then ctrls, then buttons - obj.GlobalGrid.Contents = child_handles; % Set children to new order - - obj.GlobalGrid.ColumnSizes = [180, 200, 40]; % Set column sizes - obj.GlobalGrid.Spacing = 1; - obj.GlobalGrid.RowSizes = repmat(obj.GlobalVSpacing, 1, size(obj.GlobalControls, 1)); - end - -% function swapConditions(obj, idx1, idx2) % Function started, never -% finished - MW 2017-02-15 -% % params = obj.Parameters.trial -% end - - function addEmptyConditionToParam(obj, name) - assert(obj.Parameters.isTrialSpecific(name),... - 'Tried to add a new condition to global parameter ''%s''', name); - % work out what the right 'empty' is for the parameter - currValue = obj.Parameters.Struct.(name); - if isnumeric(currValue) - newValue = zeros(size(currValue, 1), 1, class(currValue)); - elseif islogical(currValue) - newValue = false(size(currValue, 1), 1); - elseif iscell(currValue) - if numel(currValue) > 0 - if iscellstr(currValue) - % if all elements are strings, default to a blank string - newValue = {''}; - elseif isa(currValue{1}, 'function_handle') - % first element is a function handle, so create with a @nop - % handle - newValue = {@nop}; - else - % misc cell case - default to empty element - newValue = {[]}; - end - else - % misc case - default to empty element - newValue = {[]}; - end - else - error('Adding empty condition for ''%s'' type not implemented', class(currValue)); - end - obj.Parameters.Struct.(name) = cat(2, obj.Parameters.Struct.(name), newValue); - end - - function cellSelectionCallback(obj, src, eventData) - obj.SelectedCells = eventData.Indices; - if size(eventData.Indices, 1) > 0 - %cells selected, enable buttons - set(obj.MakeGlobalButton, 'Enable', 'on'); - set(obj.DeleteConditionButton, 'Enable', 'on'); - set(obj.SetValuesButton, 'Enable', 'on'); - else - %nothing selected, disable buttons - set(obj.MakeGlobalButton, 'Enable', 'off'); - set(obj.DeleteConditionButton, 'Enable', 'off'); - set(obj.SetValuesButton, 'Enable', 'off'); - end - end - - function newCondition(obj) - disp('adding new condition row'); - cellfun(@obj.addEmptyConditionToParam, obj.Parameters.TrialSpecificNames); - obj.fillConditionTable(); - end - - function deleteSelectedConditions(obj) - %DELETESELECTEDCONDITIONS Removes the selected conditions from table - % The callback for the 'Delete condition' button. This removes the - % selected conditions from the table and if less than two conditions - % remain, globalizes them. - % TODO: comment function better, index in a clearer fashion - % - % See also EXP.PARAMETERS, GLOBALISESELECTEDPARAMETERS - rows = unique(obj.SelectedCells(:,1)); - % If the number of remaining conditions is 1 or less... - names = obj.Parameters.TrialSpecificNames; - numConditions = size(obj.Parameters.Struct.(names{1}),2); - if numConditions-length(rows) <= 1 - remainingIdx = find(all(1:numConditions~=rows,1)); - if isempty(remainingIdx); remainingIdx = 1; end - % change selected cells to be all fields (except numRepeats which - % is assumed to always be the last column) - obj.SelectedCells =[ones(length(names)-1,1)*remainingIdx, (1:length(names)-1)']; - %... globalize them - obj.globaliseSelectedParameters; - obj.Parameters.removeConditions(rows) - else % Otherwise delete the selected conditions as usual - obj.Parameters.removeConditions(rows); - end - obj.fillConditionTable(); %refresh the table of conditions - end - - function globaliseSelectedParameters(obj) - [cols, iu] = unique(obj.SelectedCells(:,2)); - names = obj.TableColumnParamNames(cols); - rows = obj.SelectedCells(iu,1); %get rows of unique selected cols - arrayfun(@obj.globaliseParamAtCell, rows, cols); - obj.fillConditionTable(); %refresh the table of conditions - %now add global controls for parameters - newGlobals = gobjects(length(names),3); % Initialize object array (faster than assigning to end of array which results in two calls to constructor) - for i=length(names):-1:1 % using for loop (sorry Chris!) to initialize and populate object array 2017-02-15 MW - [newGlobals(i,1), newGlobals(i,2), newGlobals(i,3)]... % [editors, labels, buttons] - = obj.addParamUI(names{i}); - end - - idx = size(obj.GlobalControls, 1); % Calculate number of current Global params - new = numel(newGlobals); - obj.GlobalControls = [obj.GlobalControls; newGlobals]; % Add new globals to object - ggHandles = obj.GlobalGrid.Contents; - ggHandles = [ggHandles(1:idx); ggHandles((end-new+2):3:end);... - ggHandles(idx+1:idx*2); ggHandles((end-new+1):3:end);... - ggHandles(idx*2+1:idx*3); ggHandles((end-new+3):3:end)]; % Reorder them so all labels come first, then ctrls, then buttons - obj.GlobalGrid.Contents = ggHandles; % Set children to new order - - % Reset sizes - obj.GlobalGrid.RowSizes = repmat(obj.GlobalVSpacing, 1, size(obj.GlobalControls, 1)); - set(get(obj.GlobalGrid, 'Parent'),... - 'Heights', sum(obj.GlobalGrid.RowSizes)+45); % Reset height of globalPanel - obj.GlobalGrid.ColumnSizes = [180, 200, 40]; - obj.GlobalGrid.Spacing = 1; - end - - function globaliseParamAtCell(obj, row, col) - name = obj.TableColumnParamNames{col}; - value = obj.Parameters.Struct.(name)(:,row); - obj.Parameters.makeGlobal(name, value); - end - - function setSelectedValues(obj) % Set multiple fields in conditional table - disp('updating table cells'); - cols = obj.SelectedCells(:,2); % selected columns - uCol = unique(obj.SelectedCells(:,2)); - rows = obj.SelectedCells(:,1); % selected rows - % get current values of selected cells - currVals = arrayfun(@(u)obj.ConditionTable.Data(rows(cols==u),u), uCol, 'UniformOutput', 0); - names = obj.TableColumnParamNames(uCol); % selected column names - promt = cellfun(@(a,b) [a ' (' num2str(sum(cols==b)) ')'],... - names, num2cell(uCol), 'UniformOutput', 0); % names of columns & num selected rows - defaultans = cellfun(@(c) c(1), currVals); - answer = inputdlg(promt,'Set values', 1, cellflat(defaultans)); % prompt for input - if isempty(answer) % if user presses cancel - return - end - % set values for each column - cellfun(@(a,b,c) setNewVals(a,b,c), answer, currVals, names, 'UniformOutput', 0); - function newVals = setNewVals(userIn, currVals, paramName) - % check array orientation - currVals = iff(size(currVals,1)>size(currVals,2),currVals',currVals); - if strStartsWith(userIn,'@') % anon function - func_h = str2func(userIn); - % apply function to each cell - currVals = cellfun(@str2double,currVals, 'UniformOutput', 0); % convert from char - newVals = cellfun(func_h, currVals, 'UniformOutput', 0); - elseif any(userIn==':') % array syntax - arr = eval(userIn); - newVals = num2cell(arr); % convert to cell array - elseif any(userIn==','|userIn==';') % 2D arrays - C = strsplit(userIn, ';'); - newVals = cellfun(@(c)textscan(c, '%f',... - 'ReturnOnError', false,... - 'delimiter', {' ', ','}, 'MultipleDelimsAsOne', 1),... - C); - else % single value to copy across all cells - userIn = str2double(userIn); - newVals = num2cell(ones(size(currVals))*userIn); - end - - if length(newVals)>length(currVals) % too many new values - newVals = newVals(1:length(currVals)); % truncate new array - elseif length(newVals) 1) ||... % Number of rows > 1 for chars - (~ischar(obj.pStruct.(n)) && size(obj.pStruct.(n), 2) > 1)); % Number of columns > 1 for all others + strcmp(n, 'numRepeats') ||... % numRepeats always trail specific + (ischar(obj.pStruct.(n)) && size(obj.pStruct.(n), 1) > 1) ||... % Number of rows > 1 for chars + (~ischar(obj.pStruct.(n)) && size(obj.pStruct.(n), 2) > 1); % Number of columns > 1 for all others for i = 1:n name = obj.pNames{i}; obj.IsTrialSpecific.(name) = isTrialSpecificDefault(name); @@ -182,8 +182,8 @@ function makeGlobal(obj, name, newValue) 'UniformOutput', false); % concatenate trial parameter trialParamValues = cat(1, trialParamValues{:}); - if isempty(trialParamValues) % Removed MW 30.01.19 - trialParamValues = {}; + if isempty(trialParamValues) + trialParamValues = {1}; end trialParams = cell2struct(trialParamValues, trialParamNames, 1)'; globalParams = cell2struct(globalParamValues, globalParamNames, 1); diff --git a/cortexlab/+git/update.m b/cortexlab/+git/update.m index cd939a83..200ba269 100644 --- a/cortexlab/+git/update.m +++ b/cortexlab/+git/update.m @@ -28,7 +28,7 @@ function update(scheduled) % than an hour ago. if (scheduled && (weekday(now) ~= scheduled) && now - lastFetch < 7) || ... (scheduled && (weekday(now) == scheduled) && now - lastFetch < 1) || ... - (~scheduled && now - lastFetch < 1/24) + (~scheduled && now - lastFetch < 1/24) return end disp('Updating code...') diff --git a/signals b/signals index 23f93fb3..0c9a2bea 160000 --- a/signals +++ b/signals @@ -1 +1 @@ -Subproject commit 23f93fb365c441d803e7ff43b5d8f17801a409e9 +Subproject commit 0c9a2bea861352758c77a03978a874cd286835c9 From f93f73b17a48dc6bd10e91fc5cf1517e799d211d Mon Sep 17 00:00:00 2001 From: k1o0 Date: Mon, 11 Mar 2019 13:57:34 +0200 Subject: [PATCH 043/108] Updates to signals --- signals | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/signals b/signals index 2350bac3..bb6086a0 160000 --- a/signals +++ b/signals @@ -1 +1 @@ -Subproject commit 2350bac34f61f0cecab49e7fcd9fa6c055157262 +Subproject commit bb6086a019e0382a937cbe4ccf0925c1fcd8d6e5 From 9babd51a4c8529a381f8880c056c2ca0f50b310a Mon Sep 17 00:00:00 2001 From: k1o0 Date: Mon, 11 Mar 2019 16:30:59 +0200 Subject: [PATCH 044/108] Added config file for todo bot --- .github/config.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/config.yml diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 00000000..c41e8225 --- /dev/null +++ b/.github/config.yml @@ -0,0 +1,2 @@ +todo: + keyword: ['@todo','TODO','@fixme','FIXME'] From 09deaf1b4ac045413d2383ebee96484a17dd0e85 Mon Sep 17 00:00:00 2001 From: k1o0 Date: Mon, 11 Mar 2019 16:35:05 +0200 Subject: [PATCH 045/108] Hack for resize issue and fix for makeConditional() --- +eui/ConditionPanel.m | 6 ++++-- +eui/FieldPanel.m | 2 +- +eui/ParamEditor.m | 22 +++++++++++++++------- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/+eui/ConditionPanel.m b/+eui/ConditionPanel.m index eacd317f..d060abcd 100644 --- a/+eui/ConditionPanel.m +++ b/+eui/ConditionPanel.m @@ -33,12 +33,14 @@ % Create a child menu for the uiContextMenus c = uicontextmenu; obj.UIPanel.UIContextMenu = c; - obj.ContextMenus = uimenu(c, 'Label', 'Make Global', 'MenuSelectedFcn', @(~,~)obj.makeGlobal); + obj.ContextMenus = uimenu(c, 'Label', 'Make Global', ... + 'MenuSelectedFcn', @(~,~)obj.makeGlobal, 'Enable', 'off'); fcn = @(s,~)obj.ParamEditor.setRandomized(~strcmp(s.Checked, 'on')); obj.ContextMenus(2) = uimenu(c, 'Label', 'Randomize conditions', ... 'MenuSelectedFcn', fcn, 'Checked', 'on', 'Tag', 'randomize button'); obj.ContextMenus(3) = uimenu(c, 'Label', 'Sort by selected column', ... - 'MenuSelectedFcn', @(~,~)disp('feature not yet implemented'), 'Tag', 'sort by'); + 'MenuSelectedFcn', @(~,~)disp('feature not yet implemented'), ... + 'Tag', 'sort by', 'Enable', 'off'); % Create condition table p = uix.Panel('Parent', obj.UIPanel); obj.ConditionTable = uitable('Parent', p,... diff --git a/+eui/FieldPanel.m b/+eui/FieldPanel.m index 10980cb6..c0c38627 100644 --- a/+eui/FieldPanel.m +++ b/+eui/FieldPanel.m @@ -83,7 +83,7 @@ function clear(obj, idx) % FIXME Rename to clearFields function makeConditional(obj, name) if nargin == 1 - selected = obj.UIPanel.Parent.CurrentObject; %FIXME Doesn't work is parent is not figure + selected = obj.ParamEditor.Root.CurrentObject; if isa(selected, 'matlab.ui.control.UIControl') && ... strcmp(selected.Style, 'text') name = selected.String; diff --git a/+eui/ParamEditor.m b/+eui/ParamEditor.m index 4c375b7e..8a760e08 100644 --- a/+eui/ParamEditor.m +++ b/+eui/ParamEditor.m @@ -11,6 +11,7 @@ GlobalUI ConditionalUI Parent + Root Listener end @@ -23,18 +24,25 @@ end methods - function obj = ParamEditor(pars, f) + function obj = ParamEditor(pars, parent) if nargin == 0; pars = []; end if nargin < 2 - f = figure('Name', 'Parameters', 'NumberTitle', 'off',... + parent = figure('Name', 'Parameters', 'NumberTitle', 'off',... 'Toolbar', 'none', 'Menubar', 'none', 'DeleteFcn', @(~,~)obj.delete); obj.Listener = event.listener(f, 'SizeChanged', @(~,~)obj.onResize); end - obj.Parent = f; - obj.UIPanel = uix.HBox('Parent', f); + obj.Root = parent; + while ~isa(obj.Root, 'matlab.ui.Figure'); obj.Root = obj.Root.Parent; end + + obj.Parent = parent; + obj.UIPanel = uix.HBox('Parent', parent); obj.GlobalUI = eui.FieldPanel(obj.UIPanel, obj); obj.ConditionalUI = eui.ConditionPanel(obj.UIPanel, obj); obj.buildUI(pars); + % FIXME Current hack for drawing params first time + pos = obj.Root.Position; + obj.Root.Position = pos+0.01; + obj.Root.Position = pos; end function delete(obj) @@ -44,9 +52,9 @@ function delete(obj) function set.Enable(obj, value) cUI = obj.ConditionalUI; - fig = obj.Parent; + parent = obj.UIPanel; if value == true - arrayfun(@(prop) set(prop, 'Enable', 'on'), findobj(fig,'Enable','off')); + arrayfun(@(prop) set(prop, 'Enable', 'on'), findobj(parent,'Enable','off')); if isempty(cUI.SelectedCells) set(cUI.MakeGlobalButton, 'Enable', 'off'); set(cUI.DeleteConditionButton, 'Enable', 'off'); @@ -54,7 +62,7 @@ function delete(obj) end obj.Enable = true; else - arrayfun(@(prop) set(prop, 'Enable', 'off'), findobj(fig,'Enable','on')); + arrayfun(@(prop) set(prop, 'Enable', 'off'), findobj(parent,'Enable','on')); obj.Enable = false; end end From 29266f0f00c5b6db89da5d98d19317147090c35a Mon Sep 17 00:00:00 2001 From: k1o0 Date: Tue, 12 Mar 2019 14:57:22 +0200 Subject: [PATCH 046/108] Added some tests and minor modifications --- +eui/ConditionPanel.m | 48 +++++++------ +eui/FieldPanel.m | 39 +++++++---- +eui/MControl.m | 6 +- +eui/ParamEditor.m | 149 +++++++++++++++++++++------------------- +exp/Parameters.m | 6 +- tests/ParamEditorTest.m | 137 ++++++++++++++++++++++++++++++++++++ tests/ParametersTest.m | 91 ++++++++++++++++++++++++ 7 files changed, 368 insertions(+), 108 deletions(-) create mode 100644 tests/ParamEditorTest.m create mode 100644 tests/ParametersTest.m diff --git a/+eui/ConditionPanel.m b/+eui/ConditionPanel.m index d060abcd..a674f901 100644 --- a/+eui/ConditionPanel.m +++ b/+eui/ConditionPanel.m @@ -1,5 +1,5 @@ classdef ConditionPanel < handle - %UNTITLED Summary of this class goes here + %UNTITLED Deals with formatting trial conditions UI table % Detailed explanation goes here % TODO Document % TODO Add sort by column @@ -16,7 +16,7 @@ ContextMenus end - properties %(Access = protected) + properties (Access = protected) ParamEditor Listener NewConditionButton @@ -29,7 +29,8 @@ methods function obj = ConditionPanel(f, ParamEditor, varargin) obj.ParamEditor = ParamEditor; - obj.UIPanel = uix.VBox('Parent', f, 'BackgroundColor', 'white'); + obj.UIPanel = uix.VBox('Parent', f); +% obj.UIPanel.BackgroundColor = 'white'; % Create a child menu for the uiContextMenus c = uicontextmenu; obj.UIPanel.UIContextMenu = c; @@ -42,7 +43,7 @@ 'MenuSelectedFcn', @(~,~)disp('feature not yet implemented'), ... 'Tag', 'sort by', 'Enable', 'off'); % Create condition table - p = uix.Panel('Parent', obj.UIPanel); + p = uix.Panel('Parent', obj.UIPanel, 'BorderType', 'none'); obj.ConditionTable = uitable('Parent', p,... 'FontName', 'Consolas',... 'RowName', [],... @@ -53,48 +54,37 @@ 'CellEditCallback', @obj.onEdit,... 'CellSelectionCallback', @obj.onSelect); % Create button panel to hold condition control buttons - obj.ButtonPanel = uix.HBox('Parent', obj.UIPanel, ... - 'BackgroundColor', 'white'); - % Create callback so that width of button panel is slave to width of - % conditional UIPanel -% b = obj.ButtonPanel; -% fcn = @(s)set(obj.ButtonPanel, 'Position', ... -% [s.Position(1) b.Position(2) s.Position(3) b.Position(4)]); -% obj.Listener = event.listener(obj.UIPanel, 'SizeChanged', @(s,~)fcn(s)); + obj.ButtonPanel = uix.HBox('Parent', obj.UIPanel); % Define some common properties - props.BackgroundColor = 'white'; +% props.BackgroundColor = 'white'; props.Style = 'pushbutton'; props.Units = 'normalized'; props.Parent = obj.ButtonPanel; % Create out four buttons obj.NewConditionButton = uicontrol(props,... 'String', 'New condition',... - ...'Position',[0 0 1/4 1],... 'TooltipString', 'Add a new condition',... 'Callback', @(~, ~) obj.newCondition()); obj.DeleteConditionButton = uicontrol(props,... 'String', 'Delete condition',... - ...'Position',[1/4 0 1/4 1],... 'TooltipString', 'Delete the selected condition',... 'Enable', 'off',... 'Callback', @(~, ~) obj.deleteSelectedConditions()); obj.MakeGlobalButton = uicontrol(props,... 'String', 'Globalise parameter',... - ...'Position',[2/4 0 1/4 1],... 'TooltipString', sprintf(['Make the selected condition-specific parameter global (i.e. not vary by trial)\n'... 'This will move it to the global parameters section']),... 'Enable', 'off',... 'Callback', @(~, ~) obj.makeGlobal()); obj.SetValuesButton = uicontrol(props,... 'String', 'Set values',... - ...'Position',[3/4 0 1/4 1],... 'TooltipString', 'Set selected values to specified value, range or function',... 'Enable', 'off',... 'Callback', @(~, ~) obj.setSelectedValues()); obj.ButtonPanel.Widths = [-1 -1 -1 -1]; obj.UIPanel.Heights = [-1 25]; end - + function onEdit(obj, src, eventData) disp('updating table cell'); row = eventData.Indices(1); @@ -229,7 +219,7 @@ function setSelectedValues(obj) % Set multiple fields in conditional table elseif length(newVals)