Mercurial > octave
view scripts/signal/movfun.m @ 26244:58b3107a00bc
Update documentation for movXXX functions (bug #48774).
* NEWS: Announce new movXXX functions.
* octave.texi: Add new submenu "Statistics on Sliding Windows of Data".
* stats.txi: Add @DOCSTRING entries for movfun, movmin, movslice.
* scripts/signal/__parse_movargs__.m: Function moved from
signal/private/parse_moveargs.m. Use standard Octave copyright block.
* movfun.m, movslice.m: Use standard Octave copyright block. Rewrite
documentation using more Texinfo features.
* scripts/signal/module.mk: Remove movmin.m, add __parse_movargs__.m to.
build system.
* scripts/statistics/movmin.m: Function moved from scripts/signal/ dir.
Use standard Octave copyright block. Rewrite documentation using more Texinfo
features.
* scripts/statistics/module.mk: Add movmin.m to build system.
author | Juan Pablo Carbajal <ajuanpi+dev@gmail.com> |
---|---|
date | Sun, 16 Dec 2018 00:13:55 -0800 |
parents | 2d80a065ce9a |
children | 7adb62e4cc39 |
line wrap: on
line source
## Copyright (C) 2018 Juan Pablo Carbajal ## ## This file is part of Octave. ## ## Octave is free software: you can redistribute it and/or modify it ## under the terms of the GNU General Public License as published by ## the Free Software Foundation, either version 3 of the License, or ## (at your option) any later version. ## ## Octave is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Octave; see the file COPYING. If not, see ## <https://www.gnu.org/licenses/>. ## Author: Juan Pablo Carbajal <ajuanpi+dev@gmail.com> ## Created: 2018-08-09 ## -*- texinfo -*- ## @deftypefn {} {@var{y} =} movfun (@var{fun}, @var{x}, @var{wlen}) ## @deftypefnx {} {@var{y} =} movfun (@var{fun}, @var{x}, @var{[@var{nb}, @var{na}}]) ## @deftypefnx {} {@var{y} =} movfun (@dots{}, @var{property}, @var{value}) ## Apply function @var{fun} to a moving window of length @var{wlen} on data ## @var{x}. ## ## If @var{wlen} is a scalar, the function @var{fun} is applied to a moving ## window of length @var{wlen}. In this case @var{wlen} must be an odd number. ## If @var{wlen} is an array with two elements @w{@code{[@var{nb}, @var{na}]}}, ## the function is applied to a moving window @code{-@var{nb}:@var{na}}. This ## window includes @var{nb} number of elements @strong{before} the current ## element and @var{na} number of elements @strong{after} the current element. ## The current element is always included. ## ## During calculations the data input @var{x} is reshaped into a 2-dimensional ## @var{wlen}-by-@var{N} matrix and @var{fun} is called on this new matrix. ## Therefore, @var{fun} must accept an array input argument and apply the ## computation on the columns of that array. ## ## When applied to a column vector of length @var{n}, the function @var{fun} ## must return a @strong{row} vector of length @var{n}. ## When applied to an array (possibly multi-dimensional) with @var{n} columns, ## @var{fun} may return a result in either of two formats: @w{Format 1)} ## an array of size 1-by-@var{n}-by-@var{dim3}-by-@dots{}-by-@var{dimN}. This ## is the typical output format from Octave core functions. Type ## @code{demo ("movfun", 5)} for an example of this use case. ## @w{Format 2)} a row vector of length ## @code{@var{n} * @var{numel_higher_dims}} where @var{numel_higher_dims} is ## @w{@code{prod (size (@var{x})(3:end))}}. The output of @var{fun} for the ## i-th input column must be found in the output at indices ## @code{i:@var{n}:(@var{n}*@var{numel_higher_dims})}. ## This format is useful when concatenating functions into arrays, or when ## using @code{nthargout}. Type @code{demo ("movfun", 6)} for an example of ## this case. ## ## The calculation can be controlled by specifying @var{property}/@var{value} ## pairs. Valid properties are ## ## @table @asis ## ## @item @qcode{"dim"} ## Operate along the dimension specified, rather than the default of the first ## non-singleton dimension. ## ## @item @qcode{"Endpoints"} ## ## This property controls how results are calculated at the boundaries ## (@w{endpoints}) of the window. Possible values are: ## ## @table @asis ## @item @qcode{"shrink"} (default) ## The window is truncated at the beginning and end of the array to include ## only valid elements. For example, with a window of length 3, ## @code{@var{y}(1) = @var{fun} (@var{x}(1:2))}, and ## @code{@var{y}(end) = @var{fun} (@var{x}(end-1:end))}. ## ## @item @qcode{"periodic"} ## The window is wrapped around so that ## @code{@var{y}(1) = @var{fun} ([@var{x}(end-@var{k}:end), ## @var{x}(1:@var{k})])}, where @var{k} is the radius of the window. For ## example, with a window of length 3, ## @code{@var{y}(1) = @var{fun} ([@var{x}(end-1:end), @var{x}(1)])}, ## ## @item @qcode{"zero"} ## The array is pre-padded and post-padded with zeros to exactly contain the ## window. For example, with a window of length 3, ## @code{@var{y}(1) = @var{fun} ([0, @var{x}(1:2)])}, and ## @code{@var{y}(end) = @var{fun} ([@var{x}(end-1:end), 0])}. ## ## @item @qcode{"same"} ## The resulting array @var{y} has the same values as @var{x} at the ## boundaries. ## ## @item @qcode{"fill"} ## The resulting array @var{y} has @code{NaN} at the boundaries. ## ## @end table ## ## Note that for some of these values, the window size at the boundaries is not ## the same as in the middle part, and @var{fun} must work with these cases. ## ## @item @qcode{"nancond"} ## Controls whether @code{NaN} or @code{NA} values should be excluded (value: ## @qcode{"omitnan"}), or included (value: @qcode{"includenan"}) in the ## arguments passed to @var{fun}. The default is @qcode{"omitnan"}. ## ## @item @qcode{"outdim"} ## A row vector that selects which dimensions of the calculation will appear ## in the output @var{y}. This is only useful when @var{fun} returns an ## N-dimensinoal array in @w{Format 1}. The default is to return all output ## dimensions. ## ## @end table ## ## Programming Note: The property @qcode{"outdim"} can be used to save memory ## when the output of @var{fun} has many dimensions, or when a wrapper to the ## base function that selects the desired outputs is too costly. When memory ## is not an issue, the easiest way to select output dimensions is to first ## calculate the complete result with @code{movfun} and then filter that result ## with indexing. If code complexity is not an issue then a wrapper can be ## created using anonymous functions. For example, if @code{basefun} ## is a function returning a @var{K}-dimensional row output, and only ## dimension @var{D} is desired, then the following wrapper could be used. ## ## @example ## @group ## @var{fun} = @@(x) basefun (x)(:,size(x,2) * (@var{D}-1) + (1:size(x,2))); ## @var{y} = movfun (@@fun, @dots{}); ## @end group ## @end example ## ## @seealso{movslice, prepad, postpad, permute, reshape} ## @end deftypefn ## FIXME: variable names in function prototype should match documentation. function Y = movfun (func, X, wlen, varargin) persistent dispatch; valid_bc = {'open', 'periodic', 'same', 'zero', 'discard'}; if (isempty (dispatch)) dispatch = struct (); for k = valid_bc cmd = sprintf ('dispatch.%s = @%s_bc;', k{1}, k{1}); eval (cmd); endfor endif # Parse input arguments parser = inputParser (); parser.FunctionName = 'movfun'; parser.addParamValue ('Endpoints', 'open', ... @(x)any (ismember (tolower (x), valid_bc))); parser.addParamValue ('dim', [], ... @(x) isempty(x) || (isscalar (x) && x > 0 && x <= ndims(X))); parser.addParamValue ('nancond', 'omitnan', ... @(x) any (ismember (x, {'omitnan', 'includenan'}))); parser.addParamValue ('outdim', [], ... @(x) isempty(x) || (isvector (x) && all (x > 0))); parser.parse(varargin{:}); bc = parser.Results.Endpoints; % boundary condition dim = parser.Results.dim; % dimension to be used as input nancond = parser.Results.nancond; % whether nan are ignored or not outdim = parser.Results.outdim; % selected output dimension of func clear parser #### End parse input arguments # If dim was not provided search the first non-singleton dimension szX = size (X); if (isempty (dim)) dim = find (szX > 1, 1, 'first'); endif # Window length is not a vector wlen = [wpre wpos] if (isscalar (wlen)) # Check for proper window length # TODO: Matlab accepts even windows if (mod (wlen, 2) == 0) error ('Octave:invalid-input-arg', 'Window must be of odd length'); endif if (wlen == 1) error ('Octave:invalid-input-arg', 'Window must be larger than 1'); endif endif # Check that array is longer that wlen at dim. At least one full window must # fit. Function max is used to include the case when wlen is an array. # TODO: consider bc to decide what to do here if (max (wlen) > szX(dim)) error ('Octave:invalid-input-arg', ... 'window length (%d) must be shorter than length along dim (%d=%d)', ... wlen, dim, szX(dim)); endif # Move the desired dim to the 1st dimension dims = length (szX); % number of dimensions dperm = [dim 1:(dim-1) (dim+1):dims]; % permutation of dimensions X = permute (X, dperm); % permute dim to first dimensions ncols = prod (szX(dperm(2:end))); % rest dimensions as single column N = szX(dperm(1)); % length of dim X = reshape (X, N, ncols); % reshape input # obtain function for boundary conditions bcfunc = dispatch.(tolower (bc)); # obtain slicer [slc, C, Cpre, Cpos, win] = movslice (N, wlen); # Check that outdim makes sense tmp = func (zeros (length (win), 1)); % output for window noutdim = length (tmp); % number of output dimensions if (!isempty (outdim)) if (max (outdim) > noutdim) error ('Octave:invalid-input-arg', ... 'Selected output dimension (%d) is larger than output dimensions (%d)', ... max (outdim), noutdim); endif else outdim = 1:noutdim; endif soutdim = length (outdim); % length of selected output dimensions # If noutdim is not one then modify function to handle multiple outputs if (noutdim > 1) func_ = @(x) reshape (func (x), size (x, 2), noutdim)(:,outdim); else func_ = func; endif # apply processing to each column # Is it faster with cellfun? Do not think so # It could be parallel Y = zeros (N, ncols, soutdim); for i = 1:ncols Y(:,i,:) = movfun_oncol (func_, X(:,i), wlen, bcfunc, ... slc, C, Cpre, Cpos, win, soutdim); endfor # Restore shape Y = reshape (Y, [szX(dperm), soutdim]); Y = permute (Y, [dperm, dims+1]); Y = squeeze (Y); endfunction function y = movfun_oncol (func, x, wlen, bcfunc, I, C, Cpre, Cpos, win, odim) N = length (x); y = NA (N, odim); # process center part y(C,:) = func (x(I)); # process boundaries if !isempty (Cpre) % don't process zero length bkw-window y(Cpre,:) = bcfunc (func, x, Cpre, win, wlen, odim); endif if !isempty (Cpos) % don't process zero length fwd-window y(Cpos,:) = bcfunc (func, x, Cpos, win, wlen, odim); endif endfunction function y = open_bc (func, x, idxp, win, wlen, odim) N = length (x); idx = idxp + win; tf = !((idx < 1) | (idx > N)); % idx inside boundaries n = length (idxp); y = zeros (n, odim); for i = 1:n k = idx(tf(:,i),i); y(i,:) = func (x(k)); endfor endfunction function y = periodic_bc (func, x, idxp, win) N = length (x); idx = idxp + win; tf = idx < 1; idx(tf) = N + idx(tf); tf = idx > N; idx(tf) = idx(tf) - N; y = func (x(idx)); endfunction function y = same_bc (func, x, idxp, win) idx = idxp + win; idx(idx < 1) = 1; N = length (x); idx(idx > N) = N; y = func (x(idx)); endfunction function y = zero_bc (func, x, idxp, win, wlen) if isscalar (wlen) wlen = [wlen wlen]; endif N = length (x); if (min (idxp) == 1) x = prepad (x, N + wlen(1)); idx = idxp + win + wlen(1); elseif (max (idxp) == N) x = postpad (x, N + wlen(2)); idx = idxp + win; endif y = func (x(idx)); endfunction function y = discard_bc (func, x, idxp, win, wlen, odim) y = nan (length (idxp), odim); endfunction %!demo %! t = 2 * pi * linspace (0,1,100).'; %! x = sin (3 * t); %! xn = x + 0.1 * randn (size (x)); %! x_o = movfun (@mean, xn, 5, 'Endpoints', 'open'); %! x_p = movfun (@mean, xn, 5, 'Endpoints', 'periodic'); %! x_f = movfun (@mean, xn, 5, 'Endpoints', 'same'); %! x_z = movfun (@mean, xn, 5, 'Endpoints', 'zero'); %! x_d = movfun (@mean, xn, 5, 'Endpoints', 'discard'); %! %! figure (), clf %! h = plot ( %! t, xn, 'o;noisy signal;', %! t, x, '-;true;', %! t, x_o, '-;open;', %! t, x_p, '-;periodic;', %! t, x_f, '-;same;', %! t, x_z, '-;zero;', %! t, x_d, '-;discard;'); %! set (h(1), 'markerfacecolor', 'auto'); %! set (h(2:end), 'linewidth', 3); %! axis tight %! xlabel ('time') %! ylabel ('signal') %! ##### %! # Moving mean of noisy sinusoidal function with different boundary %! # conditions. %!demo %! t = 2 * pi * linspace (0,1,100).'; %! x = sin (3 * t); %! xn = x + 0.1 * randn (size (x)); %! nw = 5; %! x_ = zeros (size(x,1), nw); %! w = 3 + (1:nw) * 4; %! for i=1:nw %! x_(:,i) = movfun (@mean, xn, w(i), 'Endpoints', 'periodic'); %! endfor %! %! figure (), clf %! h = plot ( %! t, xn, 'o', %! t, x, '-', %! t, x_, '-'); %! set (h(1), 'markerfacecolor', 'auto'); %! set (h(2:end), 'linewidth', 3); %! axis tight %! xlabel ('time') %! ylabel ('signal') %! legend (h, {'noisy', 'true', strsplit(num2str(w)){:}}); %! ##### %! # Moving mean of noisy sinusoidal function with periodic boundary conditions %! # using windows of different lengths. %!demo %! t = linspace (0,1,100).'; %! X = exp(-(t - [0.1:0.3:1]).^2/2/0.1^2); %! Y = movfun (@max, X, 15); %! figure (), clf %! h = plot ( %! t, X, '-', %! t, Y, '--'); %! axis tight %! xlabel ('time') %! ylabel ('signal') %! ##### %! # Moving max of different Gaussian functions. Illustrates the application %! # on inputs with several columns. %!demo %! t = linspace (0,1-1e-2,100).'; %! w = 2 * pi * 3; %! x = sin (w * t); %! y = cos (w * t); %! y_ = movfun (@diff, x, [1 0], 'Endpoints', 'periodic'); %! # Is the same as y_ = x(2:end) - x(1:end-1); %! dt = t(2) - t(1); %! y_ = y_ / w / dt; %! figure (), clf %! h = plot (t, x, '-', %! t, y, '-', %! t, y_, ':'); %! set (h, 'linewidth', 3); %! axis tight %! xlabel ('time') %! ylabel ('signal') %! legend (h, {'sin', 'cos', 'bkw diff'}); %! ##### %! # Backward difference of sinusoidal function with periodic boundary %! # conditions. Illustrates the use of asymmetric windows. %!demo %! N = 1e3; %! wlen = 99; %! x = linspace (-1, 1, N).'; %! pp = [-2 0 1 0]; %! y = polyval (pp, x); %! yn = y + 0.1 * (abs (y) + 0.5) .* exp (randn (N, 1)); %! %! st = movfun (@(y)statistics(y).', yn, wlen); %! figure (), clf %! h = plot (x, y, '-', %! x, yn, '.', %! x, st(:,[3 6]), '-', %! x, st(:,6) + [-1 1].*st(:,7), '-', %! x, st(:,[1 2 4 5]), '-'); %! set (h([1 3:4]), 'linewidth', 3); % mean %! set (h(5:end), 'color', 'k'); %! axis tight %! xlabel ('x') %! ylabel ('y') %! legend (h, {'noiseless', 'noisy', 'mean', 'median'}) %! ##### %! # Moving window statistics. The plot highlights mean and median. black lines %! # show minimum, first quartile, third quartile, and maximum. %! # This illustrates the use of functions with multidimensional output. %!demo %! N = 1e2; %! wlen = 9; %! x = linspace (-1, 1, N).'; %! pp = [-2 0 1 0]; %! y = polyval (pp, x); %! y(:,2) = y + 0.1 * (abs (y) + 0.5) .* exp (randn (N, 1)); %! y(:,1) = -y(:,1) + 0.1 * randn (N, 1); %! %! func = @(y)[min(y) max(y)]; %! st = movfun (func, y, wlen); %! figure (), clf %! h = plot (x, y, 'o', %! x, squeeze (st(:,1,:)), '-', %! x, squeeze (st(:,2,:)), '-'); %! axis tight %! set (h(3:4), 'color', get(h(1), 'color')); %! set (h(5:6), 'color', get(h(2), 'color')); %! xlabel ('x') %! ylabel ('y') %! legend (h(1:2), {'data1', 'data2'}) %! ##### %! # Moving window bounds. %! # This illustrates the use of functions with flat multidimensional output. %!error(movfun(@min, [0;0], 1)) % wlen == 1 %!error(movfun(@min, [0;0], 2)) % odd wlen %!error(movfun(@min, [0;0], 3)) % wlen larger than data %!error(movfun(@min, [0;0;0], [1 4])) % wlen larger than data %!error(movfun(@min, [0;0;0], [4 1])) % wlen larger than data %!test %! X = (1:10).' + [-3, 0, 4]; %! ctrfun = @(x)x(2,:); %! valid_bc = {'same', 'periodic', 'zero'}; %! for bc = valid_bc %! assert (movfun(ctrfun, X, 3, 'Endpoints', bc{1}), X); %! endfor %! X_ = X; X_([1 end],:) = nan; %! assert (movfun(ctrfun, X, 3, 'Endpoints', 'discard'), X_); %! X_ = X; X_([1 end],:) = X([2 end],:); %! assert (movfun(ctrfun, X, 3, 'Endpoints', 'open'), X_); %!test % dim == 2 same as transpose %! X = randi (10, 3); %! ctrfun = @(x)x(2,:); %! valid_bc = {'same', 'periodic', 'zero'}; %! for bc = valid_bc %! assert (movfun(ctrfun, X.', 3, 'Endpoints', bc{1}, 'dim', 2), X.'); %! endfor %! X_ = X; X_([1 end],:) = nan; %! assert (movfun(ctrfun, X.', 3, 'Endpoints', 'discard', 'dim', 2), X_.'); %! X_ = X; X_([1 end],:) = X([2 end],:); %! assert (movfun(ctrfun, X.', 3, 'Endpoints', 'open', 'dim', 2), X_.'); %!test %! X = randi (10, 3, 10, 2); %! Y = movfun (@(x)x(2,:), X, 3, 'Endpoints', 'same', 'dim', 2); %! assert (X, Y); %!test # bad zero_bc %! X = ones (10, 1); %! Y = X; Y(1:2) = Y([end end-1]) = [0.6;0.8]; %! assert (movfun (@mean, X, 5, 'Endpoints', 'zero'), Y); ## Asymmetric windows %!shared X,wlen,wlen0b,wlen0f,ctrfun,Xd,UNO,UNOd0b,UNOd0f %! X = (1:10).' + [-3, 0, 4]; %! wlen = [2 1]; %! wlen0b = [0 2]; %! wlen0f = [2 0]; %! ctrfun = @(x)x(wlen(1)+1,:); %! Xd = X; Xd([1:2 end],:) = nan; %! UNO = ones (7,1); %! UNOd0b = UNOd0f = UNO; %! UNOd0b(end-1:end,:) = nan; %! UNOd0f(1:2,:) = nan; %!assert (movfun(ctrfun, X, wlen, 'Endpoints', 'same'), X); %!assert (movfun(ctrfun, X, wlen, 'Endpoints', 'discard'), Xd); %!assert (movfun(ctrfun, X, wlen, 'Endpoints', 'periodic'), X); %!assert (movfun(ctrfun, X, wlen, 'Endpoints', 'zero'), X); %!error movfun(ctrfun, X, wlen, 'Endpoints', 'open'); % for shorter x, indexing fails %!assert (movfun(@min, UNO, wlen0b, 'Endpoints', 'same'), UNO); %!assert (movfun(@min, UNO, wlen0f, 'Endpoints', 'same'), UNO); %!assert (movfun(@min, UNO, wlen, 'Endpoints', 'open'), UNO); %!assert (movfun(@min, UNO, wlen0b, 'Endpoints', 'open'), UNO); %!assert (movfun(@min, UNO, wlen0f, 'Endpoints', 'open'), UNO); %!assert (movfun(@min, UNO, wlen0b, 'Endpoints', 'discard'), UNOd0b); %!assert (movfun(@min, UNO, wlen0f, 'Endpoints', 'discard'), UNOd0f); %!assert (movfun(@min, UNO, wlen0b, 'Endpoints', 'periodic'), UNO); %!assert (movfun(@min, UNO, wlen0f, 'Endpoints', 'periodic'), UNO); %!assert (movfun(@max, UNO, wlen0b, 'Endpoints', 'zero'), UNO); %!assert (movfun(@max, UNO, wlen0f, 'Endpoints', 'zero'), UNO); ## Multidimensional output %!error(movfun(@(x)[min(x) max(x)], (1:10).', 3, 'Outdim', 3)) % outdim > dim %!assert(size(movfun(@(x)[min(x) max(x)], (1:10).', 3)), [10 2]) %!assert(size(movfun(@(x)[min(x) max(x)], cumsum(ones(10,5),2), 3)), [10 5 2])