function layer = gruProjectedLayer(numHiddenUnits, outputProjectorSize, inputProjectorSize, nameValueArgs)
%gruProjectedLayer   Gated recurrent unit (GRU) projected layer
%
%   layer = gruProjectedLayer(numHiddenUnits, outputProjectorSize, inputProjectorSize) creates a GRU projected layer.
%   numHiddenUnits is the number of hidden units in the underlying GRU operation. 
%   outputProjectorSize is the number of columns of the output projector. 
%   inputProjectorSize is the number of columns of the input projector.
%
%   layer = gruProjectedLayer(___, Name=Value) specifies additional options using one or more name-value arguments.
%
%     Name                                - Name for the layer, specified
%                                           as a character vector or a
%                                           string. 
%                                           The default value is "".
%     InputWeights                        - Input weights, specified as a
%                                           3*numHiddenUnits-by-inputProjectorSize.
%                                           The default is [].
%     RecurrentWeights                    - Recurrent weights, specified as
%                                           a 3*numHiddenUnits-by-outputProjectorSize 
%                                           matrix or []. 
%                                           The default is [].
%     Bias                                - Layer biases, specified as a
%                                           3*numHiddenUnits-by-1 vector, a 
%                                           6*numHiddenUnits-by-1 vector,
%                                           or []. The default is [].
%     InputProjector                      - Weight matrix representing the
%                                           input projector, specified by a
%                                           D-by-inputProjectorSize matrix or
%                                           [], where D is the number of
%                                           features of the input data.
%                                           The default is [].
%     OutputProjector                     - Weight matrix representing the
%                                           output projector, specified as a
%                                           numHiddenUnits-by-outputProjectorSize
%                                           matrix or [].
%                                           The default is [].
%     HiddenState                         - Initial hidden state, specified
%                                           as a numHiddenUnits-by-1 vector
%                                           or []. 
%     OutputMode                          - Output mode of the layer, specified 
%                                           as one of these options:
%                                               - "sequence" - output the full
%                                               sequence.
%                                               - "last" - output the last time
%                                               step of the sequence.
%                                           The default value is "sequence".
%     StateActivationFunction             - Activation function to update
%                                           the hidden state.
%                                           Options are:
%                                               - "tanh"
%                                               - "softsign"
%                                           The default value is "tanh".
%     GateActivationFunction              - Activation function to apply to
%                                           the gates. Options are:
%                                               - "sigmoid"
%                                               - "hard-sigmoid"
%                                           The default value is "sigmoid".
%     HasStateInputs                      - Flag indicating whether the 
%                                           layer has an extra input
%                                           that represents the layer
%                                           state. Specified as one of
%                                           the following:
%                                           - false - The layer has
%                                             a single input with the
%                                             name "in".
%                                           - true - The layer has
%                                             one additional input
%                                             with the name "hidden" which
%                                             corresponds to the hidden
%                                             state.
%                                           The default is false. When the
%                                           layer has state inputs,
%                                           HiddenState must not be
%                                           specified.
%     HasStateOutputs                     - Flag indicating whether the
%                                           layer should have an extra
%                                           output that represents the
%                                           layer state. Specified as
%                                           one of the following:
%                                           - false - The layer has
%                                             a single output with the
%                                             name "out".
%                                           - true - The layer has
%                                             one additional output
%                                             with the name "hidden", which
%                                             corresponds to the updated
%                                             hidden state.
%                                           The default is false.
%     InputWeightsLearnRateFactor         - Multiplier for the learning 
%                                           rate of the input weights,
%                                           specified as a scalar or a
%                                           three-element vector.
%                                           The default value is 1.
%     RecurrentWeightsLearnRateFactor     - Multiplier for the learning 
%                                           rate of the recurrent weights,
%                                           specified as a scalar or a
%                                           three-element vector. The
%                                           default value is 1.
%     BiasLearnRateFactor                 - Multiplier for the learning 
%                                           rate of the bias, specified as
%                                           a scalar or a three-element
%                                           vector.
%     InputProjectorLearnRateFactor       - Multiplier for the learning
%                                           rate of the input projector
%                                           weights, specified as a scalar.
%                                           The default value is 1.
%     OutputProjectorLearnRateFactor      - Multiplier for the learning
%                                           rate of the output projector
%                                           weights, specified as a scalar.
%                                           The default value is 1.
%     InputWeightsL2Factor                - Multiplier for the L2
%                                           regularizer of the input
%                                           weights, specified as a scalar
%                                           or a three-element vector.
%                                           The default value is 1.
%     RecurrentWeightsL2Factor            - Multiplier for the L2
%                                           regularizer of the recurrent
%                                           weights, specified as a scalar
%                                           or a three-element vector.
%                                           The default value is 1.
%     BiasL2Factor                        - Multiplier for the L2
%                                           regularizer of the bias,
%                                           specified as a scalar or a
%                                           three-element vector.
%                                           The default value is 0.
%     InputProjectorL2Factor              - Multiplier for the L2
%                                           regularizer of the input
%                                           projector weights,
%                                           specified as a scalar.
%                                           The default value is 1.
%     OutputProjectorL2Factor             - Multiplier for the L2
%                                           regularizer of the output
%                                           projector weights, specified as
%                                           a scalar.
%                                           The default value is 1.
%     InputWeightsInitializer             - Function to initialize the
%                                           input weights, specified as
%                                           "glorot", "he", "orthogonal",
%                                           "narrow-normal", "zeros",
%                                           "ones" or a function handle.
%                                           The default is "glorot".
%     RecurrentWeightsInitializer         - Function to initialize the
%                                           recurrent weights, specified as
%                                           "glorot", "he", "orthogonal",
%                                           "narrow-normal", "zeros",
%                                           "ones" or a function handle.
%                                           The default is "orthogonal".
%     BiasInitializer                     - Function to initialize the
%                                           bias, specified as "zeros",
%                                           "narrow-normal", "ones" or a
%                                           function handle.
%                                           The default is "zeros".
%     InputProjectorInitializer           - Function to initialize the
%                                           input projector weights,
%                                           specified as "glorot", "he",
%                                           "orthogonal", "narrow-normal",
%                                           "zeros", "ones" or a function
%                                           handle. 
%                                           The default is "orthogonal".
%     OutputProjectorInitializer          - Function to initialize the
%                                           input projector weights,
%                                           specified as "glorot", "he",
%                                           "orthogonal", "narrow-normal",
%                                           "zeros", "ones" or a function
%                                           handle. 
%                                           The default is "orthogonal".
%     ResetGateMode                       - Reset gate mode, specified as 
%                                           one of the following:
%                                               - "after-multiplication",
%                                               apply reset gate after
%                                               matrix multiplication. This
%                                               option uses the cuDNN
%                                               library when running on
%                                               GPU.
%                                               - "before-multiplication",
%                                               apply reset gate before
%                                               matrix multiplication.
%                                               - "recurrent-bias-after-multiplication",
%                                               apply reset gate after
%                                               matrix multiplication and 
%                                               use recurrent bias.
%                                           The default value is 
%                                           "after-multiplication".
%
%   Example 1:
%       % Create a GRU projected layer with 100 hidden units, an output
%       % projector size of 30, and an input projector size of 16.
%
%       layer = gruProjectedLayer(100,30,16);
%
%   Example 2:
%       % Create a GRU projected layer with 50 hidden units, an output
%       % projector size of 5, an input projector size of 8,
%       % and 12 input channels, which returns the last time step. Manually initialize the
%       % recurrent, input projector, and output projector weights using a
%       % Gaussian distribution with standard deviation 0.01.
%
%       numHiddenUnits = 50;
%       outputProjectorSize = 5;
%       inputProjectorSize = 8;
%       inputSize = 12;
%       layer = gruProjectedLayer(numHiddenUnits, outputProjectorSize, inputProjectorSize, ...
%           OutputMode="last", ...
%           RecurrentWeights=randn([3*numHiddenUnits outputProjectorSize])*0.01, ...
%           InputProjector=randn([inputSize inputProjectorSize])*0.01, ...
%           OutputProjector=randn([numHiddenUnits outputProjectorSize])*0.01);
%
%   See also nnet.cnn.layer.GRUProjectedLayer, gruLayer, compressNetworkUsingProjection.
%
%   <a href="matlab:helpview('deeplearning','list_of_layers')">List of Deep Learning Layers</a>

%   Copyright 2023 The MathWorks, Inc.

% Parse the input arguments.
arguments
    numHiddenUnits (1,1) {mustBeInteger, mustBePositive}
    outputProjectorSize (1,1) {mustBeNumeric, mustBeInteger, mustBePositive}
    inputProjectorSize (1,1) {mustBeNumeric, mustBeInteger, mustBePositive}
    nameValueArgs.Name  {nnet.internal.cnn.layer.paramvalidation.validateLayerName} = ""
    nameValueArgs.OutputMode {mustBeText} = "sequence"
    nameValueArgs.StateActivationFunction {mustBeText} = "tanh"
    nameValueArgs.GateActivationFunction {mustBeText} = "sigmoid"
    nameValueArgs.ResetGateMode {mustBeText} = "after-multiplication"
    nameValueArgs.HasStateInputs (1,1) {iAssertBinary} = false
    nameValueArgs.HasStateOutputs (1,1) {iAssertBinary} = false
    nameValueArgs.InputWeights = []
    nameValueArgs.RecurrentWeights = []
    nameValueArgs.Bias = []
    nameValueArgs.InputProjector = []
    nameValueArgs.OutputProjector = []
    nameValueArgs.InputWeightsLearnRateFactor (1,:) {iAssertValidFactor} = 1
    nameValueArgs.RecurrentWeightsLearnRateFactor (1,:) {iAssertValidFactor} = 1
    nameValueArgs.BiasLearnRateFactor (1,:) {iAssertValidFactor} = 1
    nameValueArgs.InputProjectorLearnRateFactor (1,:) {iAssertValidFactor} = 1
    nameValueArgs.OutputProjectorLearnRateFactor (1,:) {iAssertValidFactor} = 1
    nameValueArgs.InputWeightsL2Factor (1,:) {iAssertValidFactor} = 1
    nameValueArgs.RecurrentWeightsL2Factor (1,:) {iAssertValidFactor} = 1
    nameValueArgs.BiasL2Factor (1,:) {iAssertValidFactor} = 0
    nameValueArgs.InputProjectorL2Factor (1,:) {iAssertValidFactor} = 1
    nameValueArgs.OutputProjectorL2Factor (1,:) {iAssertValidFactor} = 1
    nameValueArgs.InputWeightsInitializer = "glorot"
    nameValueArgs.RecurrentWeightsInitializer = "orthogonal"
    nameValueArgs.BiasInitializer = "zeros"
    nameValueArgs.InputProjectorInitializer = "orthogonal"
    nameValueArgs.OutputProjectorInitializer = "orthogonal"
    nameValueArgs.HiddenState = iDefaultState()
end

% Canonicalization.
numHiddenUnits = double(gather(numHiddenUnits));
outputProjectorSize = double(gather(outputProjectorSize));
inputProjectorSize = double(gather(inputProjectorSize));
nameValueArgs = nnet.internal.cnn.layer.util.gatherParametersToCPU(nameValueArgs);
nameValueArgs.Name = convertStringsToChars(nameValueArgs.Name);
nameValueArgs.InputSize = [];
nameValueArgs.OutputMode = iAssertAndReturnValidOutputMode(nameValueArgs.OutputMode);
nameValueArgs.StateActivationFunction = iAssertAndReturnValidStateActivation(nameValueArgs.StateActivationFunction);
nameValueArgs.GateActivationFunction = iAssertAndReturnValidGateActivation(nameValueArgs.GateActivationFunction);
nameValueArgs.HasStateInputs = logical(nameValueArgs.HasStateInputs);
nameValueArgs.HasStateOutputs = logical(nameValueArgs.HasStateOutputs);
nameValueArgs.ResetGateMode = iAssertAndReturnValidResetGateMode(nameValueArgs.ResetGateMode);
nameValueArgs.HiddenState = iAssertAndReturnValidState(nameValueArgs.HiddenState, nameValueArgs.HasStateInputs);

% Create an internal representation of the layer.
internalLayer = nnet.internal.cnn.layer.GRUProjected(nameValueArgs.Name, ...
    numHiddenUnits, ...    
    outputProjectorSize, ...
    nameValueArgs.InputSize, ...
    inputProjectorSize, ...
    true, ...
    iGetReturnSequence(nameValueArgs.OutputMode), ...
    nameValueArgs.StateActivationFunction, ...
    nameValueArgs.GateActivationFunction, ...
    nameValueArgs.ResetGateMode, ...
    nameValueArgs.HasStateInputs, ...
    nameValueArgs.HasStateOutputs );

% Use the internal layer to construct a user visible layer.
layer = nnet.cnn.layer.GRUProjectedLayer(internalLayer);

% Set learnable parameters, learn rate, L2 factors and initializers.
layer.InputWeights = nameValueArgs.InputWeights;
layer.InputWeightsL2Factor = nameValueArgs.InputWeightsL2Factor;
layer.InputWeightsLearnRateFactor = nameValueArgs.InputWeightsLearnRateFactor;
layer.InputWeightsInitializer = nameValueArgs.InputWeightsInitializer;

layer.RecurrentWeights = nameValueArgs.RecurrentWeights;
layer.RecurrentWeightsL2Factor = nameValueArgs.RecurrentWeightsL2Factor;
layer.RecurrentWeightsLearnRateFactor = nameValueArgs.RecurrentWeightsLearnRateFactor;
layer.RecurrentWeightsInitializer = nameValueArgs.RecurrentWeightsInitializer;

layer.Bias = nameValueArgs.Bias;
layer.BiasL2Factor = nameValueArgs.BiasL2Factor;
layer.BiasLearnRateFactor = nameValueArgs.BiasLearnRateFactor;
layer.BiasInitializer = nameValueArgs.BiasInitializer;

layer.InputProjector = nameValueArgs.InputProjector;
layer.InputProjectorL2Factor = nameValueArgs.InputProjectorL2Factor;
layer.InputProjectorLearnRateFactor = nameValueArgs.InputProjectorLearnRateFactor;
layer.InputProjectorInitializer = nameValueArgs.InputProjectorInitializer;

layer.OutputProjector = nameValueArgs.OutputProjector;
layer.OutputProjectorL2Factor = nameValueArgs.OutputProjectorL2Factor;
layer.OutputProjectorLearnRateFactor = nameValueArgs.OutputProjectorLearnRateFactor;
layer.OutputProjectorInitializer = nameValueArgs.OutputProjectorInitializer;

% Set hidden state.
if ~layer.HasStateInputs
    layer.HiddenState = nameValueArgs.HiddenState;
end
end

function tf = iGetReturnSequence( mode )
tf = ~strcmp(mode, 'last');
end

function state = iDefaultState()
state = [];
end

function validString = iAssertAndReturnValidOutputMode(value)
validString = validatestring(value, {'sequence', 'last'});
end

function validString = iAssertAndReturnValidStateActivation(value)
validString = validatestring(value, {'tanh', 'softsign'});
end

function validString = iAssertAndReturnValidGateActivation(value)
validString = validatestring(value, {'sigmoid', 'hard-sigmoid'});
end

function value = iAssertAndReturnValidResetGateMode(value)
value = validatestring(value, {'after-multiplication', 'before-multiplication', 'recurrent-bias-after-multiplication'});
end

function iAssertBinary(value)
nnet.internal.cnn.options.OptionsValidator.assertBinary(value);
end

function iAssertValidFactor(value)
validateattributes(value, {'numeric'},  {'vector', 'real', 'nonnegative', 'finite'});
end

function state = iAssertAndReturnValidState(state, hasStateInputs)
% If hasStateInputs is true, then only the default state is valid
if hasStateInputs && ~isequal(state, iDefaultState())
    error( message('nnet_cnn:layer:GRUProjectedLayer:SettingStateWithStateInputs') );
end
end