function X = l1loss(X, T, W, options)
%L1LOSS   L1 loss
%
%   LOSS = L1LOSS(Y,TARGETS) computes the L1 loss between the predictions Y
%   and the target values TARGETS. Y must be a formatted dlarray. The input
%   TARGETS can be a dlarray or numeric array. The size of each dimension
%   of TARGETS must match the size of the corresponding dimension of Y. If
%   TARGETS is a formatted dlarray, its format must match the format of Y.
%   If TARGETS is not a formatted dlarray, its size must match the size of
%   Y and the function uses the same format as Y. Output LOSS is a scalar
%   unformatted dlarray.
%
%   LOSS = L1LOSS(Y,TARGETS,WEIGHTS) computes the weighted L1 loss using
%   weight values WEIGHTS, specified as a dlarray or numeric array. If
%   WEIGHTS is a vector and Y has two or more nonsingleton dimensions, then
%   WEIGHTS must be a formatted dlarray, where the dimension label of the
%   nonsingleton dimension is either 'C' or 'B' and has size matching the
%   size of the corresponding dimension in Y. If WEIGHTS is a formatted
%   dlarray with two or more nonsingleton dimensions, then its format must
%   match the format of Y. If WEIGHTS is not a formatted dlarray and it has
%   two or more nonsingleton dimensions, then its size must match the size
%   of Y and the function uses the same format as Y. If WEIGHTS is not a
%   formatted dlarray, then you can also specify the format using the
%   'WeightsFormat' option.
%
%   LOSS = L1LOSS(__,'DataFormat',FMT) computes the L1 loss between the
%   predictions Y and the target values TARGETS, where Y is an unformatted
%   dlarray or a numeric array. FMT specifies the dimension labels of the
%   input Y. FMT must be a character vector or string scalar. If TARGETS is
%   a formatted dlarray, its format must match the format FMT. The size of
%   each dimension of TARGETS must match the size of the corresponding
%   dimension of Y. If TARGETS is an unformatted dlarray or a numeric
%   array, the size of TARGETS must match the size of Y. FMT is implicitly
%   applied to TARGETS. If Y is a numeric array, TARGETS must be a dlarray.
%
%   LOSS = L1LOSS(__,'PARAM',VAL) specifies optional parameter name/value
%   pairs:
%
%      'Mask'                 - Mask indicating which elements to include
%                               for loss computation, specified as a
%                               dlarray, logical array, or numeric array.
%                               The function includes and excludes elements
%                               of the input data for loss computation when
%                               the corresponding value in the mask is 1
%                               and 0, respectively. If Mask is a formatted
%                               dlarray, its format must match that of Y.
%                               If Mask is not a formatted dlarray, the
%                               function uses the same format as Y. The
%                               size of each dimension of Mask must match
%                               the size of the corresponding dimension in
%                               Y. The default value is a logical array of
%                               ones.
%
%      'Reduction'            - Mode for reducing array of loss values,
%                               specified as one of the following:
%                               'sum'  - Sum all elements in array of loss
%                                        values. Output LOSS is a scalar
%                                        unformatted dlarray.
%                               'none' - Do not reduce array of loss
%                                        values. Output LOSS is an unformatted
%                                        dlarray with same size as Y.
%                               The default value is 'sum'.
%
%      'NormalizationFactor'  - Divisor for normalizing the output of
%                               reduced loss, specified as one of the
%                               following:
%                               'batch-size'        - Divide by number of
%                                                     observations.
%                               'all-elements'      - Divide by number of
%                                                     elements in Y.
%                               'mask-included'     - Divide by number of
%                                                     included elements in
%                                                     Y as specified by the
%                                                     mask. To use this
%                                                     option, you must also
%                                                     specify a mask using
%                                                     the 'Mask' option.
%                               'none'              - Do not normalize and
%                                                     return reduced loss. 
%                               The default value is 'batch-size'.
%
%      'DataFormat'           - Dimension labels of the input data Y,
%                               specified as a character vector or string
%                               scalar. If Y is not a formatted dlarray,
%                               then you must specify the data format.
%
%      'WeightsFormat'        - Dimension labels of WEIGHTS, specified as a
%                               character vector or string scalar. If
%                               WEIGHTS is not a formatted dlarray, then
%                               you can specify the format using weights
%                               format.
%
%   Example 1:
%       % Create input formatted dlarray
%       Y = dlarray(randn(10,32),'SSCB');
%       % Create the targets as unformatted dlarrays
%       targets = dlarray(ones(10,32));
%       % Compute L1 loss normalized by the number of observations
%       LOSS = l1loss(Y,targets);
%
%   Example 2:
%       % Create input formatted dlarray
%       Y = dlarray(randn(10,32),'CB');
%       % Create the targets as unformatted dlarrays
%       targets = dlarray(ones(10,32));
%       Create weight values as formatted dlarray
%       weights = dlarray(rand(1,32),'B');
%       % Compute weighted L1 loss without normalization
%       LOSS = l1loss(Y,targets,weights,'NormalizationFactor','none');
%
%   Example 3:
%       % Create input unformatted dlarray
%       Y = dlarray(randn(10,32));
%       % Create the targets as numeric arrays
%       targets = ones(10,32);
%       % Create Mask values
%       mask = randn(10,32)>=0.5;
%       % Compute L1 loss without reduction
%       LOSS = l1loss(Y,targets,'DataFormat','CB','Mask',mask,'Reduction','none');
%
%   Example 4:
%       % Create input formatted dlarray
%       Y = dlarray(randn(10,32),'CB');
%       % Create the targets as unformatted dlarrays
%       targets = dlarray(ones(10,32));
%       % Create weight values as numeric array
%       weights = rand(1,10);
%       % Compute L1 loss normalized by the number of elements
%       LOSS = l1loss(Y,targets,weights,'WeightsFormat','C','NormalizationFactor','all-elements');
%
%   See also L2LOSS, CROSSENTROPY, DLARRAY

%   Copyright 2021-2022 The MathWorks, Inc.

arguments
    X
    T
    W {deep.internal.dlarray.mustBeNumericDLData} = 1
    options.Mask {deep.internal.dlarray.mustBeBinaryMask}
    options.Reduction {mustBeMember(options.Reduction,["sum", "none"])}
    options.NormalizationFactor {mustBeMember(options.NormalizationFactor, ...
        ["batch-size", "all-elements", "mask-included", "none"])}
    options.DataFormat {deep.internal.dlarray.mustBeDataFormatType}
    options.WeightsFormat {deep.internal.dlarray.mustBeDataFormatType}
end

% Get default NVP arguments
[args, defaults] = iGetDefaultArguments(options);

% Validate and extract input data
[X, xfd, xdata, tdata, wdata, obsDim] = validateAndExtractLossInputData(X, T, W, args.DataFormat, args.WeightsFormat);

% Validate mask and normalization factor
[mask, normFactor] = deep.internal.dlarray.validateLossNVPArguments(xdata, xfd, args, defaults);

% Convert normalization factor values to their numeric equivalent 
normFactor = deep.internal.dlarray.convertNormalizationFactor(xdata, mask, normFactor, obsDim);

% Compute loss
loss = internal_l1loss(xdata, tdata, wdata, mask, normFactor);

% Insert data
X = deep.internal.dlarray.fromNumeric(loss, xfd, X);

% Remove dimensions from the dlarray
X = stripdims(X);

end

function [options, defaults] = iGetDefaultArguments(options)
% Default values
defaultMask = 1;
defaultReduction = "sum";
defaultNormalizationFactor = "batch-size";
defaultDataFormat = '';
defaultWeightsFormat = '';

% Initialize default flag to true
defaults.Mask = true;
defaults.Reduction = true;
defaults.NormalizationFactor = true;
defaults.DataFormat = true;
defaults.WeightsFormat = true;

if isempty(fieldnames(options))
    % Assign default values if no NVP argument is specified
    options.Mask = defaultMask;
    options.Reduction = defaultReduction;
    options.NormalizationFactor = defaultNormalizationFactor;
    options.DataFormat = defaultDataFormat;
    options.WeightsFormat = defaultWeightsFormat;
else
    nvpNames = fieldnames(defaults);
    for i = 1:numel(nvpNames)
        name = nvpNames{i};
        if ~isfield(options, name)

            switch name

                case 'Mask'
                    options.Mask = defaultMask;

                case 'Reduction'
                    options.Reduction = defaultReduction;

                case 'NormalizationFactor'
                    options.NormalizationFactor = defaultNormalizationFactor;

                case 'DataFormat'
                    options.DataFormat = defaultDataFormat;

                case 'WeightsFormat'
                    options.WeightsFormat = defaultWeightsFormat;
            end
        else
            defaults.(name) = false;
        end
    end
end

end
