function layer = convolution3dLayer( varargin )
% convolution3dLayer  3D convolution layer for Convolutional Neural Networks
%
%  layer = convolution3dLayer(filterSize, numFilters) creates a layer
%  for 3D convolution. filterSize specifies the height, width and depth of the
%  filters. It can be a scalar, in which case the filters will have the
%  same height, width and depth, or a vector [h w d] where h specifies the height
%  for the filters, and w specifies the width and d specifies the depth.
%   numFilters specifies the number of filters.
%
%  layer = convolution3dLayer(filterSize, numFilters, 'PARAM1', VAL1, 'PARAM2', VAL2, ...)
%  specifies optional parameter name/value pairs for creating the layer:
%
%    'Stride'                  - The step size for traversing the input
%                                vertically, horizontally and along the
%                                depth. This can be a scalar, in which case
%                                the same value is used for the three
%                                spatial dimensions, or it can be a vector
%                                [u v w] where u is the vertical stride, v
%                                is the horizontal stride, and w is the
%                                depth stride. The default is [1 1 1].
%    'DilationFactor'          - The step size for sampling the input or
%                                equivalently the up-sampling factor of the
%                                filter. It corresponds to an effective
%                                filter size of filterSize + (filterSize-1)
%                                * (dilationFactor-1). This can be a
%                                scalar, in which case the same value is
%                                used for all three spatial dimensions, or
%                                it can be a vector [dHeight dWidth dDepth]
%                                where dHeight is the vertical dilation,
%                                and dWidth is the horizontal dilation and
%                                dDepth is the depth dilation. The default
%                                is [1 1 1].
%    'Padding'                 - The padding applied to the input
%                                along the edges. This can be:
%                                - the character array 'same'. Padding
%                                  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
%                                  ceil(inputSize/stride), where inputSize
%                                  is the height, width and depth of the
%                                  input.
%                                - a scalar, in which case the same
%                                  padding is applied along the three
%                                  spatial dimensions
%                                - a vector [a b c] where a is the
%                                  padding applied to the top and bottom of
%                                  the input, b is the padding applied to
%                                  the left and right, and c is the padding
%                                  applied to the front and back.
%                                - a 2x3 matrix [t l f; b r bk] where t is the
%                                  padding applied to the top, b is the
%                                  padding applied to the bottom, l is the
%                                  padding applied to the left, r is the
%                                  padding applied to the right, f and bk
%                                  are the paddings applied to front and
%                                  back.
%                                Note that the padding dimensions must
%                                be less than the pooling region
%                                dimensions. The default is 0.
%    'NumChannels'             - The number of channels for each filter.
%                                If a value of 'auto' is passed in, the
%                                correct value for this parameter will be
%                                inferred at training time. The default is
%                                'auto'.
%    'Weights'                 - Layer weights, specified as a
%                                filterSize-by-numChannels-by-numFilters
%                                array or []. The default is [].
%    'Bias'                    - Layer biases, specified as a
%                                1-by-1-by-1-by-numFilters array or [].
%                                The default is [].
%    'WeightLearnRateFactor'   - A number that specifies multiplier for
%                                the learning rate of the weights. The
%                                default is 1.
%    'BiasLearnRateFactor'     - A number that specifies a multiplier
%                                for the learning rate for the biases.
%                                The default is 1.
%    'WeightL2Factor'          - A number that specifies a multiplier
%                                for the L2 weight regulariser for the
%                                weights. The default is 1.
%    'BiasL2Factor'            - A number that specifies a multiplier
%                                for the L2 weight regulariser for the
%                                biases. The default is 0.
%    'WeightsInitializer'      - The function to initialize the weights,
%                                specified as 'glorot', 'he', 'narrow-normal',
%                                'zeros', 'ones' or a function handle. The
%                                default is 'glorot'.
%    'BiasInitializer'         - The function to initialize the bias,
%                                specified as 'narrow-normal', 'zeros',
%                                'ones' or a function handle. The default
%                                is 'zeros'.
%    'Name'                    - A name for the layer. The default is ''.
%
%
%   Example 1:
%       % Create a convolutional layer with 16 filters that have a height,
%       % width and depth of 5, and use a stride of 4 in the three
%       % directions.
%
%       layer = convolution3dLayer(5, 16, 'Stride', 4);
%
%   Example 2:
%       % Create a convolutional layer with 32 filters that have a height,
%       % width and depth of 5. Pad the input volume with 2 pixels along its border.
%       % Set the learning rate factor for the bias to 2. Manually initialize the
%       % weights from a Gaussian with standard deviation 0.01.
%
%       layer = convolution3dLayer(5, 32, 'Padding', 2, ...
%           'BiasLearnRateFactor', 2, 'Weights', randn([5 5 5 3 32])*0.01);
%
%   Example 3:
%       % Create a convolutional layer with 32 filters that have a height,
%       % width and depth of 3. Set the dilation factor to 2 in all three directions.
%
%       layer = convolution3dLayer(3, 32, 'DilationFactor', 2);
%
%
%   See also nnet.cnn.layer.Convolution3DLayer, maxPooling3dLayer,
%   averagePooling3dLayer.
%
%   <a href="matlab:helpview('deeplearning','list_of_layers')">List of Deep Learning Layers</a>

%   Copyright 2018 The MathWorks, Inc.

% Parse the input arguments.
args = iParseInputArguments(varargin{:});

% Create an internal representation of a convolutional layer.
internalLayer = nnet.internal.cnn.layer.Convolution3D(args.Name, ...
                                                      args.FilterSize, ...
                                                      args.NumChannels, ...
                                                      args.NumFilters, ...
                                                      args.Stride, ...
                                                      args.DilationFactor, ...
                                                      args.PaddingMode, ...
                                                      args.PaddingSize);

internalLayer.Weights.L2Factor = args.WeightL2Factor;
internalLayer.Weights.LearnRateFactor = args.WeightLearnRateFactor;
internalLayer.Bias.L2Factor = args.BiasL2Factor;
internalLayer.Bias.LearnRateFactor = args.BiasLearnRateFactor;

% Pass the internal layer to a function to construct a user visible
% convolutional layer.
layer = nnet.cnn.layer.Convolution3DLayer(internalLayer);
layer.WeightsInitializer = args.WeightsInitializer;
layer.BiasInitializer = args.BiasInitializer;
layer.Weights = args.Weights;
layer.Bias = args.Bias;
end

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

function p = iCreateParser()
p = inputParser;
defaultStride = 1;
defaultDilationFactor = 1;
defaultPadding = 0;
defaultNumChannels = 'auto';
defaultWeightLearnRateFactor = 1;
defaultBiasLearnRateFactor = 1;
defaultWeightL2Factor = 1;
defaultBiasL2Factor = 0;
defaultName = '';
defaultWeightsInitializer = 'glorot';
defaultBiasInitializer = 'zeros';
defaultLearnable = [];

p.addParameter('Name', defaultName, @iAssertValidLayerName);
p.addRequired('FilterSize',@iAssertValidFilterSize);
p.addRequired('NumFilters',@iAssertValidNumFilters);
p.addParameter('Stride', defaultStride, @iAssertValidStride);
p.addParameter('DilationFactor', defaultDilationFactor, @iAssertValidDilationFactor);
p.addParameter('Padding', defaultPadding, @iAssertValidPadding);
p.addParameter('NumChannels', defaultNumChannels, @iAssertValidNumChannels);
p.addParameter('WeightLearnRateFactor', defaultWeightLearnRateFactor, @iAssertValidFactor);
p.addParameter('BiasLearnRateFactor', defaultBiasLearnRateFactor, @iAssertValidFactor);
p.addParameter('WeightL2Factor', defaultWeightL2Factor, @iAssertValidFactor);
p.addParameter('BiasL2Factor', defaultBiasL2Factor, @iAssertValidFactor);
p.addParameter('WeightsInitializer', defaultWeightsInitializer);
p.addParameter('BiasInitializer', defaultBiasInitializer);
p.addParameter('Weights', defaultLearnable);
p.addParameter('Bias', defaultLearnable);
end

function iAssertValidFilterSize(value)
nnet.internal.cnn.layer.paramvalidation.validate3DSizeParameter(value, 'FilterSize');
end

function iAssertValidNumFilters(value)
validateattributes(value, {'numeric'}, ...
    {'scalar','integer','positive'});
end

function iAssertValidStride(value)
nnet.internal.cnn.layer.paramvalidation.validate3DSizeParameter(value, 'Stride');
end

function iAssertValidDilationFactor(value)
nnet.internal.cnn.layer.paramvalidation.validate3DSizeParameter(value, 'DilationFactor');
end

function iAssertValidPadding(value)
numDims=3;
nnet.internal.cnn.layer.paramvalidation.validatePadding(value, numDims);
end

function iAssertValidNumChannels(value)
if(ischar(value))
    validatestring(char(value),{'auto'});
else
    validateattributes(value, {'numeric'}, ...
        {'scalar','integer','positive'});
end
end

function iAssertValidFactor(value)
nnet.internal.cnn.layer.paramvalidation.validateLearnFactor(value);
end

function iAssertValidLayerName(name)
nnet.internal.cnn.layer.paramvalidation.validateLayerName(name)
end

function inputArguments = iConvertToCanonicalForm(params)
% Make sure integral values are converted to double and strings to char vectors
inputArguments = struct;
inputArguments.FilterSize = double( iMakeIntoRowVectorOfThree(params.FilterSize) );
inputArguments.NumFilters = double( params.NumFilters );
inputArguments.Stride = double( iMakeIntoRowVectorOfThree(params.Stride) );
inputArguments.DilationFactor = double( iMakeIntoRowVectorOfThree(params.DilationFactor) );
inputArguments.PaddingMode = iCalculatePaddingMode(params.Padding);
inputArguments.PaddingSize = double( iCalculatePaddingSize(params.Padding) );
inputArguments.NumChannels = double( iConvertToEmptyIfAuto(params.NumChannels) );
inputArguments.WeightLearnRateFactor = params.WeightLearnRateFactor;
inputArguments.BiasLearnRateFactor = params.BiasLearnRateFactor;
inputArguments.WeightL2Factor = params.WeightL2Factor;
inputArguments.BiasL2Factor = params.BiasL2Factor;
inputArguments.WeightsInitializer = params.WeightsInitializer;
inputArguments.BiasInitializer = params.BiasInitializer;
inputArguments.Weights = params.Weights;
inputArguments.Bias = params.Bias;
inputArguments.Name = char(params.Name);
end

function tf = iIsRowVectorOfThree(x)
tf = isrow(x) && numel(x)==3;
end

function rowVectorOfThree = iMakeIntoRowVectorOfThree(scalarOrRowVectorOfThree)
if(iIsRowVectorOfThree(scalarOrRowVectorOfThree))
    rowVectorOfThree = scalarOrRowVectorOfThree;
else
    rowVectorOfThree = [scalarOrRowVectorOfThree scalarOrRowVectorOfThree scalarOrRowVectorOfThree];
end
end

function paddingMode = iCalculatePaddingMode(padding)
paddingMode = nnet.internal.cnn.layer.padding.calculatePaddingMode(padding);
end

function paddingSize = iCalculatePaddingSize(padding)
paddingSize = nnet.internal.cnn.layer.padding.calculatePaddingSize(padding,3);
end

function y = iConvertToEmptyIfAuto(x)
if(iIsAutoString(x))
    y = [];
else
    y = x;
end
end

function tf = iIsAutoString(x)
tf = strcmp(x, 'auto');
end
