function layer = convolution3dLayer(filterSize, numFilters, NameValueArgs)
% convolution3dLayer  3-D convolution layer
%
%  layer = convolution3dLayer(filterSize, numFilters) creates a layer
%  for 3-D 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'                  - 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'          - 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'                 - Padding applied to the input, specified
%                                as one of the following:
%                                - 'same' - Apply padding such 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 and width of the input. 
%                                - scalar - Apply padding with the 
%                                  specified size on all sides of the input.
%                                - vector [a b c] - Apply padding with size 
%                                  a on the top and bottom of the input,
%                                  size b on the left and right, and size c
%                                  on the front and back, respectively.
%                                - 2x3 matrix [t l f; b r bk] - Apply padding
%                                  with size t and b on the top and bottom 
%                                  of the input, size l and r on the left 
%                                  and right, and size f and bk on the 
%                                  front and back, respectively.
%                                The size of the padding must be less than
%                                the filter size. 
%                                The default is 0.
%    'PaddingValue'            - Value used to pad the input along the
%                                edges, specified as one of the
%                                following:
%                                - Scalar value - Pad using the specified 
%                                  value.
%                                - 'symmetric-include-edge' - Pad using 
%                                  mirrored layer input, including the edge
%                                  values.
%                                - 'symmetric-exclude-edge' - Pad using 
%                                  mirrored layer input, excluding the edge
%                                  values.
%                                - 'replicate' - Pad using repeated layer 
%                                  input border elements.
%                                The default value 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'   - Multiplier for the learning rate of the 
%                                weights. The default is 1.
%    'BiasLearnRateFactor'     - Multiplier for the learning rate of the 
%                                biases. The default is 1.
%    'WeightL2Factor'          - Multiplier for the L2 weight regulariser 
%                                for the weights. The default is 1.
%    'BiasL2Factor'            - Multiplier for the L2 weight regulariser 
%                                for the biases. The default is 0.
%    'WeightsInitializer'      - Function to initialize the weights,
%                                specified as 'glorot', 'he',
%                                'narrow-normal', 'zeros', 'ones' or a
%                                function handle. The default is
%                                'glorot'.
%    'BiasInitializer'         - Function to initialize the bias,
%                                specified as 'narrow-normal', 'zeros',
%                                'ones' or a function handle. The
%                                default is 'zeros'.
%    'Name'                    - 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-2023 The MathWorks, Inc.

arguments
    filterSize                           {iAssertValidFilterSize}
    numFilters                           {iAssertValidNumFilters}
    NameValueArgs.Name                   {iAssertValidLayerName} = ''
    NameValueArgs.Stride                 {iAssertValidStride} = 1
    NameValueArgs.DilationFactor         {iAssertValidDilationFactor} = 1
    NameValueArgs.Padding                {iAssertValidPadding} = 0
    NameValueArgs.PaddingValue           {iAssertValidPaddingValue} = 0
    NameValueArgs.NumChannels            {iAssertValidNumChannels} = 'auto'
    NameValueArgs.WeightLearnRateFactor  {iAssertValidFactor} = 1
    NameValueArgs.BiasLearnRateFactor    {iAssertValidFactor} = 1
    NameValueArgs.WeightsInitializer     = 'glorot'
    NameValueArgs.BiasInitializer        = 'zeros'
    NameValueArgs.WeightL2Factor         {iAssertValidFactor} = 1
    NameValueArgs.BiasL2Factor           {iAssertValidFactor} = 0
    NameValueArgs.Weights                = []
    NameValueArgs.Bias                   = []
end

% Store all input arguments in one struct
args = NameValueArgs;
args.FilterSize = filterSize;
args.NumFilters = numFilters;

% Gather arguments to CPU and convert them to canonical form
args = nnet.internal.cnn.layer.util.gatherParametersToCPU(args);
args = iConvertToCanonicalForm(args);

% 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, ...
                                                      args.PaddingValue);

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 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 iAssertValidPaddingValue(value)
nnet.internal.cnn.layer.padding.assertAndReturnValidPaddingValue(value);
end

function value = iConvertPaddingValue(value)
value = nnet.internal.cnn.layer.padding.assertAndReturnValidPaddingValue(value);
end

function iAssertValidNumChannels(value)
nnet.internal.cnn.layer.paramvalidation.validateNumChannels(value);
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 = iConvertToDouble( iMakeIntoRowVectorOfThree(params.FilterSize) );
inputArguments.NumFilters = iConvertToDouble( params.NumFilters );
inputArguments.Stride = iConvertToDouble( iMakeIntoRowVectorOfThree(params.Stride) );
inputArguments.DilationFactor = iConvertToDouble( iMakeIntoRowVectorOfThree(params.DilationFactor) );
inputArguments.PaddingMode = iCalculatePaddingMode(params.Padding);
inputArguments.PaddingSize = iConvertToDouble( iCalculatePaddingSize(params.Padding) );
inputArguments.PaddingValue = iConvertPaddingValue(params.PaddingValue);
inputArguments.NumChannels = iConvertToDouble( 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

function value = iConvertToDouble(value)
value = nnet.internal.cnn.layer.util.convertToDouble(value);
end
