function X = dltranspconv(X, weights, bias, varargin)
%DLTRANSPCONV  Deep learning transposed convolution 
%
%    Y = DLTRANSPCONV(X,WEIGHTS,BIAS) computes the deep learning transposed
%    convolution of the input data X using filters defined by WEIGHTS and
%    adds a constant BIAS. X must be a formatted dlarray. Inputs WEIGHTS
%    and BIAS can be formatted dlarrays, unformatted dlarrays, or numeric
%    arrays. Output Y is a formatted dlarray with the same dimension labels
%    as X.
%
%    If WEIGHTS is an unformatted dlarray or a numeric array, it must be
%    specified as a filterSize-by-numFiltersPerGroup-by-numChannelsPerGroup
%    -by-numGroups array, where filterSize must have as many entries as
%    the number of spatial dimensions of X. If numGroups is greater than 1,
%    the function performs grouped transposed convolutions. Otherwise,
%    numGroups is singleton and can be omitted. The size of the 'C'
%    dimension of output Y is the product of numFiltersPerGroup
%    and numGroups.
%
%    If WEIGHTS is a formatted dlarray, it can have multiple spatial
%    dimensions labeled 'S', one channel dimension labeled 'C', and up to
%    two other dimensions labeled 'U'. The number of 'S' dimensions must
%    match the number of 'S' dimensions of input X.
% 
%    If WEIGHTS contains less than two 'U' dimensions, DLTRANSPCONV
%    performs a transposed convolution with no groups. The size of the 'U'
%    dimension must match the size of the 'C' dimension of input X.
%    The 'C' dimension specifies the number of filters. The size of the 'C'
%    dimension of output Y is equal to the number of filters.
%
%    If WEIGHTS contain two 'U' dimensions, DLTRANSPCONV performs a grouped
%    transposed convolution. The 'C' dimension specifies the number of
%    filters per group. The second 'U' dimension specifies the number of
%    groups. The first 'U' dimension of WEIGHTS must be equal to the size
%    of the 'C' dimension of input X divided by the number of groups.
%    The size of the 'C' dimension of output Y is the product of the size
%    of the 'C' and the size of the second 'U' dimension of WEIGHTS.
%
%    BIAS can be a numeric array or a formatted or unformatted dlarray with
%    up to one non-singleton dimension. If BIAS is a scalar or has only
%    singleton dimensions, the same bias is applied to each output.
%    Otherwise, the number of elements must match the number of filters
%    specified by WEIGHTS. If BIAS is a formatted dlarray, the non-
%    -singleton dimension must be labeled 'C'.
% 
%    Y = DLTRANSPCONV(X,WEIGHTS,BIAS,'DataFormat',FMT) computes the deep
%    learning transposed convolution of an unformatted dlarray or a numeric
%    array X. FMT specifies the dimension labels of the input X. If
%    the input X is a numeric array, at least one of WEIGHTS or BIAS must
%    be a dlarray. The output Y is an unformatted dlarray with the same
%    dimension order as X.
%
%    Y = DLTRANSPCONV(_,'PARAM1',VAL1,'PARAM2',VAL2,...) specifies optional
%    parameter name/value pairs:
%
%      'Stride'                  - Step size for traversing the input,
%                                  specified as one of the following
%                                  - a scalar, in which case the same value
%                                     is used for all spatial dimensions.
%                                  - a vector [s1,...,sd] where si is
%                                    the stride along the ith dimension.
%                                  The default value is 1.
%      'DilationFactor'          - Step size for sampling the input or
%                                  equivalently the up-sampling factor of
%                                  the filter. Corresponds to an effective
%                                  filter size of filterSize +
%                                  + (filterSize-1) * (dilationFactor-1).
%                                  Specified as one of the following
%                                  - a scalar, in which case the same value
%                                     is used for all spatial dimensions.
%                                  - a vector [c1,...,cd] where ci is the
%                                    dilation along the ith dimension.
%                                  The default value is 1.
%      'Cropping'                - Amount to trim the edges of the full
%                                  transposed convolution, specified as
%                                  one of the following:
%                                  - 'same'. Cropping is set so that
%                                    the output size is the same as
%                                    the input size when the stride is 1.
%                                    More generally, the output size is
%                                    inputSize*stride, where
%                                    inputSize is the size of the input
%                                    along a spatial dimension.
%                                  - a scalar, in which case the same value
%                                     is used for all spatial dimensions.
%                                  - a vector [p1,...,pd] where pi is the
%                                    cropping applied to the start and end
%                                    along the ith spatial dimension.
%                                  - a matrix [low1,...,lowd;high1,...,
%                                    highd] of size 2-by-d where lowi and
%                                    highi are the cropping along the lower
%                                    and upper edges of the ith spatial
%                                    dimension.
%                                  The default value is 0.
%      'DataFormat'              - Dimension labels of the input data X,
%                                  specified as a char array or string.
%                                  Required if X is not a formatted dlarray.
%
%
% Example 1:
%     % Create input data as formatted dlarray with 3 channels
%     X = dlarray(ones([64 64 3]),'SSC');
%
%     % Create weights as formatted dlarray representing 96 filters
%     % with height and width of 11
%     weights = dlarray(ones([11 11 96 3]),'SSCU');
%
%     % Compute deep learning transposed convolution
%     dltranspconv(X, weights, 0)
%
% Example 2:
%     % Create input data as formatted dlarray representing a batch of 7
%     % 64-by-64 activations with 8 channels
%     X = dlarray(ones([64 64 8 7]),'SSCB');
% 
%     % Create weights as numeric array representing 2 groups of filters
%      % with 3 filters per group
%      weights = ones([5 5 3 4 2]);
% 
%      % Compute deep learning transposed convolution
%      dltranspconv(X,weights,0)
%  
% Example 3:
%      % Create input data as numeric array representing a batch of 7
%      % 64-by-64 activations with 8 channels
%      X = ones([64 64 8 7]);
% 
%      % Create weights as formatted dlarray representing 8 groups of
%      % filters with 1 filter per group
%      weights = dlarray(ones([5 5 1 1 8]),'SSCUU');
% 
%      % Compute deep learning transposed convolution
%      dltranspconv(X,weights,0,'DataFormat','SSCB')
%
%   See also AVGPOOL, DLARRAY, DLCONV, MAXPOOL, MAXUNPOOL

%   Copyright 2019-2020 The MathWorks, Inc.

narginchk(3, inf);

% Parse Name-Value pairs with no validation
args = iParseInputArguments(varargin{:});

% Ensure X is a dlarray and include DataFormat
[X, perm] = deep.internal.dlarray.validateDataFormatArg(X, args.DataFormat);

% Perform the internal permutations to obtain X in the builtin order
X = permuteExplicitly(X);

% Extract labels
labels = dims(X);

% Currently the dltranspconv supports only 1D, 2D, and 3D convolution.
nSpatialDimsInX = count(labels, 'S');
if nSpatialDimsInX == 0 || nSpatialDimsInX > 3
    error(message('deep:dlarray:ConvUnsupportedNumberOfSpatialDimensions'))
end

% Find the spatial size of X
sizesSpatialDimsInX = size(X, 1:nSpatialDimsInX);

% Locate channel dimension and find number of channels of X
channelDimInX = find(labels == 'C');
isChannelDimInX = ~isempty(channelDimInX);
if isChannelDimInX
    nbChannelsInX = size(X, channelDimInX);
else
    nbChannelsInX = 1;
end

% Extract data in the format needed by builtin convolution functions.
fd = X.FormattedData;
X.FormattedData = [];
[fd,xdata] = extractData(fd);

% Extract weights & bias
[weights, nbFiltersPerGroup, nbGroups, isWeightsLabeled] = iExtractWeights(weights, nSpatialDimsInX, nbChannelsInX);
bias = iExtractBias(bias, nSpatialDimsInX, nbFiltersPerGroup*nbGroups, isWeightsLabeled);

% The dlarray methods should not accept logical inputs 
if islogical(weights) || islogical(bias)
    error(message('deep:dlarray:LogicalsNotSupported'));
end

% Grouped convolution not supported for 3D 
if nbGroups~=1 && nSpatialDimsInX>2
    error(message('deep:dlarray:Conv3DWithGroups'))
end

% Convolution that creates 'C' dimension that was not there in the
% input data is not supported.
if ~isChannelDimInX && nbFiltersPerGroup*nbGroups~=1
    error(message('deep:dlarray:ConvWeightsWithFiltersXWithNoChannel'))
end

% Find the filter size.
filterSize = iFindFilterSize(weights, nSpatialDimsInX);

% Validate Name-Value pairs
[args, outputSizeOffset] = iValidateInputArguments(args, nSpatialDimsInX, sizesSpatialDimsInX, filterSize);

[xdata, inputXShape] = iReshapeXToBuiltinFormat(xdata, nSpatialDimsInX, sizesSpatialDimsInX, nbChannelsInX);

% Call internal API
xdata = internal_dltranspconv(xdata, weights, bias,...
    args.CroppingSize, args.Stride, args.DilationFactor, nbGroups, outputSizeOffset);

% Reshape X back to the user format
xdata = iReshapeXBackToUserFormat(xdata, inputXShape, nSpatialDimsInX, isChannelDimInX, nbFiltersPerGroup*nbGroups);

% Insert the FormattedData back in the dlarray
fd = insertData(fd, xdata);
X.FormattedData = fd;

% Remove dimensions from the dlarray if it was passed as unformatted.
if ~isempty(args.DataFormat)
    X = stripdims(X);
    
    % Permutes back in the same order as DataFormat
    X = ipermute(X, perm);
end
end

function [croppingSize, outputSizeOffset] = iCalculateSameCroppingAndOutputSizeOffsetIfNeeded(nSpatialDimsInX, sizesSpatialDimsInX, filterSize, croppingSize, stride, dilation)
if nSpatialDimsInX == 1
    % Currently cnn.layer does not support 1D padding.
    sizesSpatialDimsInX = [sizesSpatialDimsInX 1];
    filterSize = [filterSize 1];
end

if isempty(croppingSize)
    % Same cropping case: Set cropping size and outputSizeOffset for
    %    a given stride and filter size in such a way that
    %    1) The outputSize is stride*inputSize
    %    2) Cropping is minimum
    effectiveFilterSize = (filterSize-1).*dilation + 1;
    KMinusS = effectiveFilterSize - stride;
    outputSizeOffset = max(0, -KMinusS);
    totalCropping = KMinusS + outputSizeOffset;
    
    croppingSize = [floor(totalCropping/2); ceil(totalCropping/2)];
    croppingSize = croppingSize(:).';
else
    % Manual cropping case: find minimum outputSizeOffset such that the
    % output size is positive.
    outputSizeOffset = iComputeMinOutputSizeOffset(sizesSpatialDimsInX, filterSize, croppingSize, stride, dilation);
end
end

function outputSizeOffset = iComputeMinOutputSizeOffset(sizesSpatialDimsInX, filterSize, croppingSize, stride, dilation)
% If cropping not same, compute dynamically
% outputSizeOffset, as the minimum allowed value.
% This is the minimum value such that
% 1) 0 <= outputSizeOffset < stride
% 2) The output size is positive.
totalCropping = croppingSize(1:2:end) + croppingSize(2:2:end);
outputSizeWithoutOffset = stride .* (sizesSpatialDimsInX-1) + filterSize.*dilation - (dilation-1) - totalCropping;
minOutputSizeOffset = max(1 - outputSizeWithoutOffset, 0);

if any(minOutputSizeOffset >= stride)
    % If such value does not exist, error.
    error(message('deep:dlarray:TranspConvInvalidInputSize'));
else
    outputSizeOffset = minOutputSizeOffset;
end
end

function [weights, nbFiltersPerGroup, nbGroups, isWeightsLabeled] = iExtractWeights(weights, nSpatialDimsInX, nbChannelsInX)
isWeightsLabeled = false;
if isa(weights, 'dlarray')
    fd = weights.FormattedData;
    if ~isempty(fd.DimensionMetadata)
        isWeightsLabeled = true;
        % If weights are formatted dlarray
        % Perform the internal permutations to obtain X in the builtin order
        weights = permuteExplicitly(weights);
        fd = weights.FormattedData;
        
        % Find number of spatial dims in weights
        spatialDimsInW = finddatadim(weights, 'S');
        nSpatialDimsInW = nnz(spatialDimsInW);
        
        % Ensure no 'T' or 'B' dims
        if(~isempty(finddatadim(weights,'T')) || ~isempty(finddatadim(weights,'B')))
            error(message('deep:dlarray:ConvWeightsContainBatchDim'))
        end
        
        % Extract weights in the format needed by builtin convolution functions.
        [fd,weights] = extractData(fd);
        
        % Retrieve 'C' dim index and find the number of filters
        channelDim = channelIndex(fd);
        nbFiltersPerGroup = size(weights, channelDim);
        
        % Behaviors for different number of 'U' dims
        otherDims = find(fd.DimensionMetadata == 'U');
        if(numel(otherDims) == 0)
            nbGroups = 1;
            nbChannelsInW = 1;
        elseif(numel(otherDims) == 1)
            nbGroups = 1;
            % The 'U' dim encodes number of channels
            nbChannelsInW = size(weights, otherDims);
        elseif(numel(otherDims) == 2)
            % The first 'U' dim encodes number of channels per group
            nbGroups = size(weights, otherDims(2));
            % The second 'U' dim encodes number of groups
            nbChannelsInW = nbGroups * size(weights, otherDims(1));
        else
            % If more than 2 'U' dims
            error(message('deep:dlarray:ConvWeightsHaveMultipleOtherDims'))
        end
        
        % If W different number of spatial dims than X
        if(nSpatialDimsInW > nSpatialDimsInX)
            %If W contains too many spatial dims error
            error(message('deep:dlarray:ConvWeightsHaveTooManySpatial', nSpatialDimsInW, nSpatialDimsInX));
        end
    else
        % If weights are unformatted dlarray
        % Extract data in the format needed by builtin convolution functions.
        [~,weights] = extractData(fd);
        
        [nSpatialDimsInW, nbFiltersPerGroup, nbChannelsInW, nbGroups] = iSizesOfNumericWeights(weights, nSpatialDimsInX);
    end
else
    % If weights are numeric array
    deep.internal.dlarray.validateNonDlarray(weights);
    [nSpatialDimsInW, nbFiltersPerGroup, nbChannelsInW, nbGroups] = iSizesOfNumericWeights(weights, nSpatialDimsInX);
end

weights = iReshapeWeights(weights, nSpatialDimsInX, nSpatialDimsInW, nbFiltersPerGroup, nbChannelsInW);

% Channels in W must match channels in X
if (nbChannelsInW ~= nbChannelsInX)
    nbChannelsPerGroupInW = nbChannelsInW / nbGroups;
    if(nbGroups==1)
        if isWeightsLabeled
            error(message('deep:dlarray:TranspConvLabeledWeightsChannelMismatch', nbChannelsInX, nbChannelsPerGroupInW))
        else
            error(message('deep:dlarray:TranspConvChannelMismatch', nbChannelsInX, nbChannelsPerGroupInW, nSpatialDimsInX+2))
        end
    else
        if isWeightsLabeled
            error(message('deep:dlarray:TranspConvLabeledWeightsChannelsPerGroupMismatch', nbChannelsInX, nbChannelsPerGroupInW, nbGroups))
        else
            error(message('deep:dlarray:TranspConvChannelsPerGroupMismatch', nbChannelsInX, nbChannelsPerGroupInW, nSpatialDimsInX+2, nbGroups, nSpatialDimsInX+3))
        end
    end
end
end

function [nSpatialDimsInW, nbFilters, nbChannelsInW, nbGroups] = iSizesOfNumericWeights(weights, nSpatialDimsInX)

% For unformatted weights number of spatial dimensions is taken to be
% the same for weights and the data input
nSpatialDimsInW = nSpatialDimsInX;

if isempty(weights)
    error(message('deep:dlarray:ConvEmptyWeights'))
end

nDimsInW = ndims(weights);
if(nDimsInW > nSpatialDimsInX+3)
    error(message('deep:dlarray:ConvWeightsHaveTooManyDims', nSpatialDimsInX))
end

if(nDimsInW > nSpatialDimsInX+2)
    % The case with groups encoded in the last dim
    nbGroups = size(weights, nSpatialDimsInX+3);
else
    % The case with no groups
    nbGroups = 1;
end

if(nDimsInW > nSpatialDimsInX+1)
    % The number of channels dim is just after filter dim
    nbChannelsInW  = nbGroups * size(weights, nSpatialDimsInX+2);
else
    nbChannelsInW = 1;
end

if(nDimsInW > nSpatialDimsInX)
    % The filter dim is just after spacial dims
    nbFilters = size(weights, nSpatialDimsInX+1);
else
    nbFilters = 1;
end
end

function weights = iReshapeWeights(weights, nSpatialDimsInX, nSpatialDimsInW, nbFiltersPerGroup, nbChannelsInW)
sizeWeights = size(weights, 1:nSpatialDimsInX);

if nSpatialDimsInW == nSpatialDimsInX
    % The builtin format combines filter and group dimensions.
    newSizeWeights = [sizeWeights(1:nSpatialDimsInX) nbFiltersPerGroup nbChannelsInW];
else
    %If W contains too few 'S' dims use singleton expansion
    newSizeWeights = [sizeWeights(1:nSpatialDimsInW) ones(1,nSpatialDimsInX-nSpatialDimsInW)  nbFiltersPerGroup nbChannelsInW];
end

% Dltranspconv supports 1D by inserting extra singleton spatial dimension.
if nSpatialDimsInX==1
    newSizeWeights = [sizeWeights(1:nSpatialDimsInW) 1  nbFiltersPerGroup nbChannelsInW];
end

weights = reshape(weights, newSizeWeights);
end

function [xdata, inputXShape] = iReshapeXToBuiltinFormat(xdata, nSpatialDimsInX, sizesSpatialDimsInX, nbChannelsInX)
inputXShape = size(xdata);

if nSpatialDimsInX~=1
    % builtin shape combines filter and group dimensions
    builtinFormat = num2cell([sizesSpatialDimsInX(1:nSpatialDimsInX) nbChannelsInX]);
else
    % Dltranspconv supports 1D by inserting extra spatial dimension.
    builtinFormat = num2cell([sizesSpatialDimsInX(1:nSpatialDimsInX) 1 nbChannelsInX]);
end
xdata = reshape(xdata, builtinFormat{:}, []);
end

function xdata = iReshapeXBackToUserFormat(xdata, inputXShape, nSpatialDimsInX, isChannelDimInX, nbFilters)
outputXShape = size(xdata, 1:nSpatialDimsInX);

if isChannelDimInX
    batchDims = inputXShape(nSpatialDimsInX+2:end);
    userFormat = num2cell([outputXShape nbFilters batchDims]);
else
    % Convolution that creates 'C' dimension that was not there in the
    % input data is not supported.
    batchDims = inputXShape(nSpatialDimsInX+1:end);
    userFormat = num2cell([outputXShape batchDims]);
end

xdata = reshape(xdata, userFormat{:});
end

function bias = iExtractBias(bias, nSpatialDims, nbFilters, isWeightsLabeled)

% Extract bias if wrapped in a dlarray.
if isa(bias, 'dlarray')
    bias = permuteExplicitly(bias);
    [fd,bias] = extractData(bias.FormattedData);
    
    % If formatted dlarray
    if ~isempty(fd.DimensionMetadata)
        channelIdx = channelIndex(fd);
        
        % Bias must contain a 'C' dimension and it must not contain other
        % non-singleton dimension apart from the 'C' dimensions
        if ~contains(fd.DimensionMetadata,'C') || ...
            (~isscalar(bias) && size(bias, channelIdx) ~= numel(bias))
            error(message('deep:dlarray:ConvBiasWithWrongDims'));
        end
    end
else
    deep.internal.dlarray.validateNonDlarray(bias);
end

nbElementsBias = numel(bias);
if(nbElementsBias == 1)
    % Scalar bias is interpreted as constant vector.
    bias = bias(ones(1, nbFilters));
elseif nnz(size(bias) ~= 1) > 1
    % Bias cannot have more than one non-singleton dimension.
    error(message('deep:dlarray:ConvNonScalarOrVectorBias'));
elseif(nbElementsBias ~= nbFilters)
    % The number of elements of Bias must equal the number of filters.
    if isWeightsLabeled
        error(message('deep:dlarray:TranspConvLabeledWeightsMismatchBias', nbElementsBias, nbFilters));
    else
        error(message('deep:dlarray:TranspConvWeightsMismatchBias', nbElementsBias, nbFilters, nSpatialDims+1));
    end
end

% Reshape to builtin format.
bias = iReshapeBias(bias, nSpatialDims);
end

function bias = iReshapeBias(bias, nSpatialDims)
if nSpatialDims~=1
    % Reshape bias to 1 x 1 x ... x C format.
    spatialPart(1:nSpatialDims) = {1};
else
    % Dltranspconv supports 1D by inserting extra spatial dimension.
    spatialPart(1:(nSpatialDims+1)) = {1};
end
bias = reshape(bias, spatialPart{:}, []);
end

function inputArguments = iParseInputArguments(varargin)
varargin = nnet.internal.cnn.layer.util.gatherParametersToCPU(varargin);
parser = iCreateParser();
parser.parse(varargin{:});
inputArguments = parser.Results;
end

function p = iCreateParser()
p = inputParser;
defaultStride = 1;
defaultDilationFactor = 1;
defaultCropping = 0;
defaultFormat = [];

p.addParameter('Stride', defaultStride);
p.addParameter('DilationFactor', defaultDilationFactor);
p.addParameter('Cropping', defaultCropping);
p.addParameter('DataFormat', defaultFormat, @(x)validateattributes(x, {'char', 'string'}, {'nonempty'}));
end

function [args, outputSizeOffset] = iValidateInputArguments(args, dims, sizesSpatialDimsInX, filterSize)
iAssertValidStride(args.Stride, dims);
iAssertValidDilationFactor(args.DilationFactor, dims);
iAssertValidCropping(args.Cropping, dims);

[args, outputSizeOffset] = iConvertToCanonicalForm(args, dims, sizesSpatialDimsInX, filterSize);
end

function iAssertValidStride(value,dims)
validateattributes(value, {'numeric'}, ...
    {'positive', 'real', 'integer', 'nonempty'});

if ~(isscalar(value) || iIsRowVector(value,dims))
    error(message('deep:dlarray:ConvStrideMustBeScalarOrVector'));
end
end

function iAssertValidDilationFactor(value,dims)
validateattributes(value, {'numeric'}, ...
    {'positive', 'real', 'integer', 'nonempty'});

if ~(isscalar(value) || iIsRowVector(value,dims))
    error(message('deep:dlarray:ConvDilationMustBeScalarOrVector'));
end
end

function iAssertValidCropping(padding, dims)
deep.internal.dlarray.assertValidPaddingOrCropping(padding, dims, 'Cropping');
end

function [inputArguments, outputSizeOffset] = iConvertToCanonicalForm(args, dims, sizesSpatialDimsInX, filterSize)
% Accont for dilation.
effectiveFilterSize = (filterSize-1).*args.DilationFactor + 1;

% Make sure integral values are converted to double and strings to char
% vectors.
inputArguments = struct;
if dims ~= 1
    inputArguments.Stride = double(iMakeIntoRowVector(args.Stride,dims));
    inputArguments.DilationFactor = double(iMakeIntoRowVector(args.DilationFactor,dims));
else
    % Dltranspconv supports 1D by inserting extra spatial dimension.
    inputArguments.Stride = [double(args.Stride) 1];
    inputArguments.DilationFactor = [double(args.DilationFactor) 1];
end

inputArguments.DataFormat = args.DataFormat;

% At this point args.Cropping has been validated it isValidStringOrCharArray
% it must be 'same' padding or equivalent.
if nnet.internal.cnn.layer.paramvalidation.isValidStringOrCharArray(args.Cropping)
    % Dltranspconv calculates 'same' cropping with 
    % iCalculateSameCroppingAndOutputSizeOffsetIfNeeded instead of 
    % deep.internal.dlarray.calculatePaddingSize like other dlmethods.
    inputArguments.CroppingSize = [];
else
    inputArguments.CroppingSize = double(deep.internal.dlarray.calculatePaddingSize(args.Cropping,dims,effectiveFilterSize,inputArguments.Stride,sizesSpatialDimsInX));
end
[inputArguments.CroppingSize, outputSizeOffset] = ...
    iCalculateSameCroppingAndOutputSizeOffsetIfNeeded(dims, sizesSpatialDimsInX, filterSize, inputArguments.CroppingSize, inputArguments.Stride, inputArguments.DilationFactor);
end

function rowVector = iMakeIntoRowVector(scalarOrRowVector,dims)
if(iIsRowVector(scalarOrRowVector,dims))
    rowVector = scalarOrRowVector;
else
    rowVector = repmat(scalarOrRowVector,1,dims);
end
end

function tf = iIsRowVector(x,dims)
tf = isrow(x) && numel(x)==dims;
end

function filterSize = iFindFilterSize(weights, nSpatialDims)
filterSize = size(weights, 1:nSpatialDims);
end
