function Y = activations(this, varargin)
% activations   Compute network layer activations
%
%   Y = activations(net, X, layerOut) returns network
%   activations for a specific layer using the network net and
%   the data X. Network activations are computed by forward
%   propagating the input X through the network up to the
%   specified layer. Specify layerOut as a character vector
%   corresponding to the name of a layer. If the layer has
%   multiple outputs, specify  layerOut as the name of the
%   layer, followed by the  '/' character, followed by the name
%   of the layer output.
%
%   For a network with a single image input layer, specify X as
%   one of the following:
%       - A 4-D array of images, where the first three
%         dimensions correspond to the height, width, and
%         channels of an image, and the fourth dimension
%         indexes the individual images.
%       - A 5-D array of images, where the first four
%         dimensions correspond to the height, width, depth and
%         channels of an image, and the fifth dimension
%         indexes the individual images.
%       - A datastore, where the output of the datastore read
%         function is one image, a cell array of images, or a
%         table whose first column contains images.
%       - A table, where the first column contains either image
%         paths or images.
%
%   For a network with a sequence input layer, X must be:
%       - A numeric array representing a single sequence.
%       - A cell array of sequences.
%       - A datastore, where the output of the datastore read function is
%         a cell array or a table containing sequences in the first
%         column.
%   Each sequence must be either:
%       - A vector sequence, specified by a C-by-S matrix, where C is the
%         number of features and S is the number of time steps.
%       - A 2-D image sequence, specified by an H-by-W-by-C-by-S array,
%         where H-by-W-by-C is the 2-D image size and S is the number of
%         time steps.
%       - A 3-D image sequence, specified by an H-by-W-by-D-by-C-by-S
%         array, where H-by-W-by-D-by-C is the 3-D image size and S is the
%         number of time steps.
%
%   For a network with a feature input layer, X must be:
%       - A single observation specified as a row vector.
%       - A 2-D array of observations, where the first dimension indexes
%         the observations and the second dimension indexes the features.
%       - A datastore, where the output of the datastore read function is
%         a cell array or a table containing feature vectors in the first
%         column.
%       - A table, where each column represents a feature of the dataset.
%
%   For networks with an image input layer, Y is by default either an
%   H-by-W-by-C-by-N array or an H-by-W-by-D-by-C-by-N array for 2-D or 3-D
%   images repectively, where H, W, D and C are the height, width, depth
%   and number of channels for the chosen output as applicable. Each
%   H-by-W-by-C or H-by-W-by-D-by-C sub-array is the output for a single
%   observation. For more information, see the 'OutputAs' name-value pair
%   argument.
%
%   For networks with a sequence input layer, the format of Y depends on
%   whether layerOut has sequence output:
%       - For sequence output, Y is a cell array of size N-by-1 where N is
%         the number of observations specified in X. The contents of each
%         cell array will be either a C-by-S, H-by-W-by-C-by-S or
%         H-by-W-by-D-by-C-by-S array. The 'OutputAs' name-value pair
%         cannot be 'rows' or 'columns' for sequence output.
%       - For non-sequence output, Y is a numeric array of size C-by-N,
%         H-by-W-by-C-by-N or H-by-W-by-D-by-C-by-N.
%
%   For networks with a feature input layer, Y is numeric array of size
%   C-by-N.
%
%   Y = activations(net, cds, layerOut) computes the activations for a
%   multiple input network with the combined datastore cds. The combined
%   datastore must return a 1-by-N cell array, where N is the number of
%   network inputs.
%
%   Y = activations(net, X1, ..., XN, layerOut) computes the activations
%   for a multiple input network with N inputs, where the input data is
%   X1, ..., XN.
%
%   Y = activations(__, 'PARAM1', VAL1, ...) specifies optional name-value
%   pairs described below:
%
%       'OutputAs'          - Format of output activations, specified
%                             as one of the following:
%                               - 'channels' - The output is either a C-by-N
%                                 array, a H-by-W-by-C-by-N array or a
%                                 H-by-W-by-D-by-C-by-N array, where H, W,
%                                 D and C are the height, width, depth and
%                                 number of channels for the output of the
%                                 chosen layer as applicable, and N indexes
%                                 each observation. If the layer returns a
%                                 sequence, then the output is a N-by-1
%                                 cell array containing arrays of size
%                                 C-by-S, H-by-W-by-C-by-S or
%                                 H-by-W-by-D-by-C-by-S.
%                               - 'rows' - The output is an N-by-M matrix,
%                                 where N is the number of observations,
%                                 and M is the number of output elements in
%                                 the chosen layer. Each row of the matrix
%                                 is the output for a single observation.
%                                 This option does not support layers with
%                                 sequence output.
%                               - 'columns' - The output is an M-by-N
%                                 matrix, where M is the number of output
%                                 elements from the chosen layer, and N is
%                                 the number of observations. Each column
%                                 of the matrix is the output for a single
%                                 observation. This option does not support
%                                 layers with sequence output.
%                             The default is 'channels'.
%
%       'MiniBatchSize'         - The size of the mini-batches for
%                                 computing predictions. Larger
%                                 mini-batch sizes lead to faster
%                                 predictions, at the cost of more
%                                 memory. The default is 128.
%
%       'ExecutionEnvironment'  - The execution environment for the
%                                 network. This determines what hardware
%                                 resources will be used to run the
%                                 network.
%                               - 'auto' - Use a GPU if it is
%                                 available, otherwise use the CPU.
%                               - 'gpu' - Use the GPU. To use a
%                                  GPU, you must have Parallel
%                                 Computing Toolbox(TM) and a supported 
%                                 GPU device. If a suitable GPU is not 
%                                 available, activations returns an error
%                                 message.
%                               - 'cpu' - Use the CPU.
%                               - 'multi-gpu' - Use multiple GPUs on one
%                                 machine, using a local parallel pool.
%                                 If no pool is open, one is opened with
%                                 one worker per supported GPU device.
%                               - 'parallel' - Use a compute cluster. If
%                                 no pool is open, one is opened using
%                                 the default cluster profile. If the
%                                 pool has access to GPUs then they will
%                                 be used and excess workers will be
%                                 idle. If the pool has no GPUs then
%                                 activations will take place on all
%                                 cluster CPUs.
%                               The default is 'auto'.
%
%       'Acceleration'
%                               - Optimizations that can improve
%                                 performance at the expense of some
%                                 overhead on the first call and possible
%                                 additional memory usage.
%                               - 'auto' - Automatically select optimizations
%                                 suitable for the input network and
%                                 environment.
%                               - 'mex' - (GPU only) Generate and execute a
%                                 MEX function. Subsequent calls with the same
%                                 network and options use the MEX function.
%                                 This option is not supported when 
%                                 'ExecutionEnvironment' value is either 
%                                 'parallel' or 'multi-gpu'.
%                               - 'none' - Disable all acceleration.
%                               The default is 'auto'.
%
%       'SequenceLength'        - Strategy to determine the length of the
%                                 sequences used per mini-batch. Options
%                                 are:
%                                   - 'longest' to pad all sequences in a
%                                     batch to the length of the longest
%                                     sequence.
%                                   - 'shortest' to truncate all sequences
%                                     in a batch to the length of the
%                                     shortest sequence.
%                                   - Positive integer - For each
%                                     mini-batch, pad the sequences to the
%                                     nearest multiple of the specified
%                                     length that is greater than the
%                                     longest sequence length in the
%                                     mini-batch, and then split the
%                                     sequences into smaller sequences of
%                                     the specified length. If splitting
%                                     occurs, then the software creates
%                                     extra mini-batches.
%                                 The default is 'longest'.
%
%       'SequencePaddingValue'  - Scalar value used to pad sequences where
%                                 necessary. The default is 0.
%
%       'SequencePaddingDirection'
%                               - Direction of padding or truncation,
%                                 specified as:
%                                  - 'right' - Pad or truncate sequences
%                                  on the right.
%                                  - 'left' - Pad or truncate sequences on
%                                  the left.
%                                 The default is 'right'.
%
%   See also DAGNetwork/predict, DAGNetwork/classify.

%   Copyright 2018-2022 The MathWorks, Inc.

narginchk(3,inf)

if ~isscalar(this)
    error(message('nnet_cnn:DAGNetwork:NonScalarNetwork'));
end

if this.NumInputLayers == 1
    % Network has one input
    X = varargin{1};
    layerOut = varargin{2};
    nameValuePairs = varargin(3:end);
elseif iDataForMultipleInputNetworkPassedAsOneArgument(nargin-2, varargin)
    if iIsCombinedOrTransformedDatastore(varargin)
        % Network has multiple inputs, but the inputs are passed in as one
        % argument.
        X = varargin{1};
        layerOut = varargin{2};
        nameValuePairs = varargin(3:end);
    else
        error( message( 'nnet_cnn:DAGNetwork:InvalidPredictDataForMultipleInputNetwork', this.NumInputLayers) );
    end
elseif iDataForMultipleInputNetworkPassedAsMultipleArguments(nargin-2, varargin, this.NumInputLayers)
    % Network has multiple inputs, and the inputs are passed in as multiple
    % arguments.
    X = varargin(1:this.NumInputLayers);
    layerOut = varargin{this.NumInputLayers+1};
    nameValuePairs = varargin((this.NumInputLayers+2):end);
else
    error( message( 'nnet_cnn:DAGNetwork:InvalidPredictDataForMultipleInputNetwork', this.NumInputLayers) );
end

% Validate source
iValidateIsStringOrCharArray(layerOut);

% Convert source character vector to layer index and output name.
[~, layerIndex, layerOutputName, layerOutputIndex] = ...
    iGetSourceInformation(layerOut, this.Layers);

% Get the layerIndex in topological order
layerIndex = find(this.getTopologicalOrder() == layerIndex);

% Get the type of the layer
layer = this.PrivateNetwork.SortedLayers{layerIndex};

% Error out certain outputs
if isa(layer, 'nnet.internal.cnn.layer.MaxPooling2D') && any(strcmp(layerOutputName,{'indices','size'}))
    error(message('nnet_cnn:DAGNetwork:CannotGetIndicesOrSize'));
elseif isa(layer, 'nnet.internal.cnn.layer.SequenceFolding') && strcmp(layerOutputName,{'miniBatchSize'})
    error(message('nnet_cnn:DAGNetwork:CannotGetFoldingMiniBatchSize'));
end

% Calculate activations
try
    Y = this.calculateActivations(X, layerIndex, layerOutputIndex, nameValuePairs{:});
catch ex
    if ex.identifier == "nnet_cnn:internal:cnn:layer:FullyConnected:InvalidInputData"
        ex = MException(message(...
            'nnet_cnn:DAGNetwork:ImageSizeBreaksFullyConnectedLayerForActivations', ...
            ex.message));
    end
    nnet.internal.cnn.util.rethrowDLExceptions(ex)
end
end

function iValidateIsStringOrCharArray(x)
if ~iIsValidStringOrCharArray(x)
    error(message('nnet_cnn:DAGNetwork:ActivationLayerOutputMustBeString'));
end
end

function tf = iIsValidStringOrCharArray(x)
tf = nnet.internal.cnn.layer.paramvalidation.isValidStringOrCharArray(x);
end

function [startLayerName, startLayerIndex, layerOutputName, layerOutputIndex] = iGetSourceInformation(s, layers)
iValidateThereAreNotMultipleForwardSlashesInSource(s);
sSplit = strsplit(s, '/');
startLayerName = sSplit{1};
iValidateLayerName( startLayerName, layers );
startLayerIndex = iConvertLayerNameToIndex( startLayerName, layers );
if numel(sSplit) == 2
    layerOutputName = sSplit{2};
    hasOutputNames = iHasNonEmptyOutputNames( layers(startLayerIndex) );
else
    iThrowErrorIfLayerHasMultipleOutputs( layers(startLayerIndex) );
    [layerOutputName, hasOutputNames] = iGetFirstOutputNameIfItExists( layers(startLayerIndex) );
end
layerOutputIndex = iGetLayerOutputIndex( layers(startLayerIndex), layerOutputName, hasOutputNames );
end

function iValidateThereAreNotMultipleForwardSlashesInSource(s)
if iContainsMoreThanOneBackslash(s)
    error(message('nnet_cnn:DAGNetwork:ActivationLayerOutputCannotHaveMultipleForwardSlashes'));
end
end

function tf = iContainsMoreThanOneBackslash(x)
tf = length(strfind(x, '/')) > 1;
end

function layerIndex = iConvertLayerNameToIndex(layerName, externalLayers)
layerNames = {externalLayers.Name}';
layerIndex = find(strcmp(layerNames, layerName));
end

function iValidateLayerName(layerName, externalLayers)
if ~iLayerNameExists(layerName, externalLayers)
    error(message('nnet_cnn:DAGNetwork:LayerDoesNotExist', layerName))
end
end

function tf = iLayerNameExists(layerName, externalLayers)
layerNames = {externalLayers.Name}';
tf = any(strcmp(layerNames, layerName));
end

function iThrowErrorIfLayerHasMultipleOutputs(layer)
if isprop(layer, 'NumOutputs') && (layer.NumOutputs > 1)
    error(message('nnet_cnn:DAGNetwork:MustSpecifyOutputForMultipleOutputLayer', layer.Name));
end
end

function [layerOutputName, hasOutputNames] = iGetFirstOutputNameIfItExists(layer)
hasOutputNames = iHasNonEmptyOutputNames(layer);
if hasOutputNames
    layerOutputName = layer.OutputNames{1};
else
    % Output layers do not have output names, so in this case we assign a
    % default output name
    layerOutputName = iDefaultOutputNameForOutputLayers();
end
end

function layerOutputIndex = iGetLayerOutputIndex(layer, layerOutputName, hasOutputNames)
if hasOutputNames
    iValidateLayerOutputName(layer.Name, layerOutputName, layer.OutputNames);
    layerOutputIndex = find(strcmp(layer.OutputNames, layerOutputName));
else
    % Output layers do not have output names, so in this case we expect the
    % default name which we have previously assigned
    iValidateLayerOutputName(layer.Name, layerOutputName, iDefaultOutputNameForOutputLayers());
    layerOutputIndex = 1;
end
end

function name = iDefaultOutputNameForOutputLayers()
name = 'out';
end

function iValidateLayerOutputName(layerName, layerOutputName, validOutputNames)
if ~any(strcmp(layerOutputName, validOutputNames))
    error(message('nnet_cnn:DAGNetwork:NonExistentLayerOutput', layerName, layerOutputName));
end
end

function tf = iDataForMultipleInputNetworkPassedAsOneArgument( ...
    numArguments, arguments )
tf = (numArguments == 1) || ...
    iLayerOutArgumentShouldBeAtThisPosition(numArguments, arguments, 2);
end

function tf = iIsCombinedOrTransformedDatastore(arguments)
tf = isa(arguments{1},'matlab.io.datastore.CombinedDatastore') || ...
    isa(arguments{1},'matlab.io.datastore.TransformedDatastore');
end

function tf = iDataForMultipleInputNetworkPassedAsMultipleArguments( ...
    numArguments, arguments, numInputs )
tf = (numArguments == numInputs) || ...
    iLayerOutArgumentShouldBeAtThisPosition(numArguments, arguments, numInputs+1);
end

function tf = iLayerOutArgumentShouldBeAtThisPosition(numArguments, arguments, position)
% Note that this check takes place before we parse arguments, so it does
% not definitively prove that layerOut is at this position (i.e. the test
% is necessary but not sufficient). However, in the event that there is a
% string in this position, but it is not an N-V pair, the parser will find
% the error.
tf = (numArguments >= position) && ...
    iFirstStringOrArrayIsAtThisPosition(arguments, position);
end

function tf = iFirstStringOrArrayIsAtThisPosition(inputCell, position)

firstStringOrCharArray = find( ...
    cellfun(@iIsValidStringOrCharArray, inputCell), 1 );

if isempty(firstStringOrCharArray)
    tf = false;
else
    tf = ( position == firstStringOrCharArray );
end

end

function tf = iHasNonEmptyOutputNames(layer)
% Return true when a layer is an output layer
tf = isprop(layer, 'OutputNames') && ~isempty(layer.OutputNames);
end
