classdef DAGNetwork < nnet.cnn.network.HeterogeneousDAGNetwork & matlab.mixin.CustomDisplay & ...
        deep.internal.quantization.QuantizedNetworkDisplay
    % DAGNetwork   A Directed Acyclic Graph Network
    %
    %   A DAG network can have its layers arranged in a directed acyclic
    %   graph.
    %
    %   DAGNetwork properties:
    %       Layers          - The layers of a network
    %       Connections     - The connections between the layers
    %       InputNames      - The names of the input layers
    %       OutputNames     - The names of the output layers
    %
    %   DAGNetwork methods:
    %       predict                 - Run the network on input data
    %       classify                - Classify data with a network
    %       activations             - Compute specific network layer activations
    %       predictAndUpdateState   - Predict on data and update network state
    %       classifyAndUpdateState  - Classify data and update network state
    %       resetState              - Reset network state
    %       plot                    - Plot a diagram of the network
    %       layerGraph              - Convert to a layer graph object
    %
    %   Example:
    %       Train a network on synthetic digit data, and measure its
    %       accuracy:
    %
    %       [XTrain, YTrain] = digitTrain4DArrayData;
    %
    %       layers = [
    %           imageInputLayer([28 28 1], 'Name', 'input')
    %           convolution2dLayer(5, 20, 'Name', 'conv_1')
    %           reluLayer('Name', 'relu_1')
    %           convolution2dLayer(3, 20, 'Padding', 1, 'Name', 'conv_2')
    %           reluLayer('Name', 'relu_2')
    %           convolution2dLayer(3, 20, 'Padding', 1, 'Name', 'conv_3')
    %           reluLayer('Name', 'relu_3')
    %           additionLayer(2, 'Name', 'add')
    %           fullyConnectedLayer(10, 'Name', 'fc')
    %           softmaxLayer('Name', 'softmax')
    %           classificationLayer('Name', 'classoutput')];
    %
    %       lgraph = layerGraph(layers);
    %
    %       lgraph = connectLayers(lgraph, 'relu_1', 'add/in2');
    %
    %       plot(lgraph);
    %
    %       options = trainingOptions('sgdm', 'Plots', 'training-progress');
    %       [net,info] = trainNetwork(XTrain, YTrain, lgraph, options);
    %
    %       [XTest, YTest] = digitTest4DArrayData;
    %       YPred = classify(net, XTest);
    %       accuracy = sum(YTest == YPred)/numel(YTest)
    %
    %   See also LayerGraph.
    
    %   Copyright 2017-2023 The MathWorks, Inc.
    
    properties(Access = private)
        % PrivateNetwork
        PrivateNetwork
        
        % LayersMap
        LayersMap
        
        % NetworkInfo
        NetworkInfo
    end
    
    properties(Dependent, SetAccess = private)
        % Layers   The layers in the network
        %   An array of the layers in the network. Each layer has different
        %   properties depending on what type of layer it is.
        Layers
        
        % Connections   A table of connections between the layers
        %   A table with one row for each connection between two layers.
        %   The table has two columns:
        %       Source          - The name of the layer (and the layer
        %                         output if applicable) where the
        %                         connection begins.
        %       Destination     - The name of the layer (and the layer
        %                         input if applicable) where the connection
        %                         ends.
        Connections
        
        % InputNames   Names of the input layers of the network
        InputNames
        
        % OutputNames   Names of the output layers of the network
        OutputNames
    end
    
    properties(Dependent, Hidden, SetAccess = private)
        % SortedLayers   Layers in a topologically sorted order
        %   An array of the layers in the network. Each layer has different
        %   properties depending on what type of layer it is.
        SortedLayers
        
        % SortedConnections   Connections in a topologically sorted order
        %   A table with one row for each connection between two layers.
        %   The table has two columns:
        %       Source          - The name of the layer (and the layer
        %                         output if applicable) where the
        %                         connection begins.
        %       Destination     - The name of the layer (and the layer
        %                         input if applicable) where the connection
        %                         ends.
        SortedConnections
        
        % NumInputLayers   The number of input layers in the network
        NumInputLayers
    end
    
    properties(Dependent, Access = private)
        % Internal LayerGraph with trained values of learnable parameters
        LayerGraph
    end
    
    properties(Transient, Access = private)
        % PredictEnvironment Handle cache to store prediction environment
        %                    variables. The internal value is a struct
        %                    composed by the following fields:
        %                    - ExecutionEnvironment
        %                    - GPUShouldBeUsed
        %                    - PredictNetwork
        PredictEnvironment
        
        % LayersCache   Cached version of the Layers property for fast
        %               access
        LayersCache
        
        % dlAccel_map   Handle Map to store dlAccel_info objects.
        %               dlAccel_info is a value class containing necessary
        %               information to execute generated MEX file.
        dlAccel_map % TODO Remove dlaccel legacy code g2224358
        
        % MexNetworkCache   Cache of the MexNetwork generated for this
        %                   network.
        MexNetworkCache

        % IsQuantized Flag to determine whether the network is quantized
        IsQuantized = false;
    end
    
    methods
        function val = get.Layers(this)
            if this.LayersCache.isEmpty()
                internalLayers = this.PrivateNetwork.OriginalLayers;
                % Strip the QuantizationInfo from the layers so that the
                % quantization information is not carried into different
                % network constructions.
                internalLayers = nnet.internal.cnn.layer.util.resetQuantizationInfo(internalLayers);
                
                this.LayersCache.fillCache( ...
                    this.LayersMap.externalLayers( internalLayers) );
            end
            val = this.LayersCache.Value;
        end
        
        function val = get.Connections(this)
            % Using OriginalLayers and OriginalConnections from
            % PrivateNetwork.
            val = nnet.internal.cnn.util.internalToExternalConnections( ...
                this.PrivateNetwork.OriginalConnections, ...
                this.PrivateNetwork.OriginalLayers);
        end
        
        function val = get.SortedLayers(this)
            % Using topologically sorted Layers from PrivateNetwork.
            val = this.LayersMap.externalLayers( this.PrivateNetwork.SortedLayers );
        end
        
        function val = get.SortedConnections(this)
            % Using topologically sorted Layers and Connections from
            % PrivateNetwork.
            val = nnet.internal.cnn.util.internalToExternalConnections( ...
                this.PrivateNetwork.SortedConnections, ...
                this.PrivateNetwork.SortedLayers);
        end
        
        function val = get.NumInputLayers(this)
            val = this.PrivateNetwork.NumInputLayers;
        end
        
        function val = get.LayerGraph(this)
            val = this.PrivateNetwork.LayerGraph;
        end
        
        function val = get.InputNames(this)
            % Get input layer names
            val = cellfun( @(l)l.Name, ...
                this.PrivateNetwork.InputLayers, ...
                'UniformOutput', false );
            % Return row vector
            val = val(:)';
        end
        
        function val = get.OutputNames(this)
            % Get output layer names
            val = cellfun( @(l)l.Name, ...
                this.PrivateNetwork.OutputLayers, ...
                'UniformOutput', false );
            % Return row vector
            val = val(:)';
        end
    end
    
    methods(Hidden)
        function this = DAGNetwork(internalDAGNetwork, layersMap)
            % DAGNetwork   Constructor for the DAG network
            
            this.PrivateNetwork = internalDAGNetwork;
            this.LayersMap = layersMap;
            this.PredictEnvironment = nnet.internal.cnn.layer.learnable.CacheHandle();
            this.LayersCache = nnet.internal.cnn.layer.learnable.CacheHandle();
            this.MexNetworkCache = nnet.internal.cnn.coder.MexNetworkCache();
            this.NetworkInfo = this.PrivateNetwork.computeNetworkInfo();
            
            this.dlAccel_map = nnet.internal.cnn.coder.dlAccel_map(); % TODO Remove dlaccel legacy code g2224358
            this.IsQuantized = deep.internal.quantization.isQuantizationEnabled(internalDAGNetwork);
        end
    end
    
    methods(Hidden)
        function layerGraph = getLayerGraph(this)
            % getLayerGraph   Get an internal layer graph for this
            % network after training
            layerGraph = this.LayerGraph;
        end
        
        function layerGraph = getOptimizedLayerGraph(this)
            % getOptimizedLayerGraph  Get the internal optimized layer
            % graph for this network, from the prediction cache if
            % necessary
            if this.PredictEnvironment.isEmpty
                internalNetwork = this.PrivateNetwork;
            else
                internalNetwork = this.PredictEnvironment.Value.PredictNetwork;
            end
            layerGraph = nnet.internal.cnn.LayerGraph( ...
                internalNetwork.Layers, internalNetwork.Connections );
        end
        
        function layersMap = getLayerMap(this)
            % getLayersMap   Get the layer map for this network
            layersMap = this.LayersMap;
        end
        
        function topologicalOrder = getTopologicalOrder(this)
            topologicalOrder = this.PrivateNetwork.TopologicalOrder;
        end
        
        % return the output size for a given layeridx
        % layeridx is an index in the topologically sorted array of layers
        % inputSizes       - is a length M cell array specifying the
        %                    input sizes for layers i_1, i_2, ..., i_M in
        %                    that order. Where the input has m input layers
        %                    and they appear in positions i_1, i_2... upto
        %                    i_M in the topologically sorted list. Set an
        %                    element to empty to take the size from the
        %                    matching input layer.
        % outputAsFeatures - If true, returns sizes and formats for spatial
        % (Default: true)    and channel dimensions only. If false, batch
        %                    and sequence dimensions may be added if not
        %                    provided in the input and may be NaN to
        %                    indicate variable size support.
        %
        % layerOutputSize  - is the output size for layer layeridx. If
        %                    layer layeridx has multiple outputs then
        %                    layerOutputSize is a cell array of output
        %                    sizes for layer layeridx.
        % layerOutputFormat- also provides the semantics of each output
        %                    dimension as a format char vector containing
        %                    the labels 'SCBTU'.
        function [layerOutputSize, layerOutputFormat] = getOutputSize(...
                this, layeridx, inputSizes, outputAsFeatures)
            arguments
                this
                layeridx
                inputSizes
                outputAsFeatures = true
            end
            
            if ~iscell(inputSizes)
                inputSizes = {inputSizes};
            end
            
            % Some input layers internally accept a different input data
            % format from that accepted by predict/activations, and it is
            % the dispatcher that converts the input. We need to take the
            % role of the dispatcher in these cases.
            inputLayers = this.PrivateNetwork.InputLayers;
            for i = 1:numel(inputSizes)
                layer = inputLayers(i);
                
                % Numeric inputs to feature input layers are expected in
                % BxC format and are converted to CxB by the dispatcher
                if nnet.internal.cnn.util.isFeatureInput(layer{1}.InputData)
                    if numel(inputSizes{i}) == 1
                        inputSizes{i} = [1 inputSizes{i}];
                    elseif numel(inputSizes{i}) == 2
                        inputSizes{i} = flip(inputSizes{i}, 2);
                    end
                end
                
                % Numeric inputs to sequence input layers do not accept a
                % batch dimension, and this is inserted by the dispatcher
                % as the second-last dimension
                if nnet.internal.cnn.util.hasSequenceInput(layer)
                    sz = inputSizes{i};
                    n = numel(sz);
                    numFeatureDims = numel(layer{1}.InputSize);
                    if numel(sz) > numFeatureDims
                        inputSizes{i} = [sz(1:n-1) 1 sz(n)];
                    end
                end
            end
                    
            % Propagate through the internal API
            [layerOutputSize, layerOutputFormat] = ...
                this.PrivateNetwork.inferOutputSizesGivenInputSizes(...
                inputSizes, layeridx, outputAsFeatures);
            
            % For sequence networks the output batch dimension is removed
            % and converted to an outer cell array
            for i = 1:numel(layerOutputSize)                
                if iscell(layerOutputFormat{i})
                    % Multi-output layers output cells from
                    % inferOutputSizesGivenInputSizes
                    for j = 1:numel(layerOutputFormat{i})
                        [layerOutputSize{i}{j},layerOutputFormat{i}{j}] = iRemoveBTDimension(layerOutputSize{i}{j},layerOutputFormat{i}{j});
                    end
                else
                    [layerOutputSize{i},layerOutputFormat{i}] = iRemoveBTDimension(layerOutputSize{i},layerOutputFormat{i});
                end
            end
            
            % Unwrap from cell array if a single output
            if isscalar(layerOutputSize)
                layerOutputSize = layerOutputSize{1};
                layerOutputFormat = layerOutputFormat{1};
            end
        end
        
        % Multi-input, multi-output activations
        varargout = activationsMIMO(this, X, varargin)
        
        function Y = activationsSeries(this, X, layerID, varargin)
            % activationsSeries   Get activations from a SeriesNetwork
            layerID = nnet.internal.cnn.util.validateNetworkLayerNameOrIndex(layerID, this.Layers);
            Y = this.calculateActivations(X, layerID, 1, varargin{:});
        end
        
        function dlAccelInfo = getdlAccelInfo(this, dlKey)
            % getdlAccelInfo This is a hidden method that returns
            % dlAccel_map information corresponding to the input key
            % TODO Remove dlaccel legacy code g2224358
            dlAccelInfo = dl_getValue(this.dlAccel_map, dlKey);
        end
        
        function internalDAGNetwork = getInternalDAGNetwork(this)
            % getInternalDAGNetwork This is a hidden method that returns
            % the underlying internal dag representation of the network
            internalDAGNetwork = this.PrivateNetwork;
        end
        
        function tf = isequal(this, net, varargin)
            % isequal   Check equality of DAGNetwork objects
            %
            %   tf = isequal(net1,net2) checks equality of the DAGNetwork
            %   or SeriesNetork objects net1 and net2. The function returns
            %   1 (true) when the properties and architectures match and 0
            %   (false) otherwise.
            %
            %   Note that if one input is a DAGNetwork or SeriesNetwork
            %   object, and the other is a LayerGraph object, then the
            %   function returns 0 (false).
            %
            %   tf = isequal(net1,...,netN) checks equality of the N
            %   DAGNetwork or SeriesNetwork objects net1, …, netN.
            
            tf = iCompare(this, net, @isequal, varargin{:});
        end
        
        function tf = isequaln(this, net, varargin)
            % isequaln   Check equality of DAGNetwork objects ignoring NaN values
            %
            %   tf = isequaln(net1,net2) checks equality of the DAGNetwork
            %   or SeriesNetwork objects net1 and net2, ignoring NaN
            %   values. The function returns 1 (true) when the properties
            %   and architectures match and 0 (false) otherwise.
            %
            %   Note that if one input is a DAGNetwork or SeriesNetwork
            %   object, and the other is a LayerGraph object, then the
            %   function returns 0 (false).
            %
            %   tf = isequaln(net1,...,netN) checks equality of the N
            %   DAGNetwork or SeriesNetwork objects net1, …, netN, ignoring
            %   NaN values.
            
             tf = iCompare(this, net, @isequaln, varargin{:});
        end
    end
    
    methods (Access = {?nnet.internal.cnn.coder.MexNetworkCache,?SeriesNetwork})
        function mexNetworkCache = getMexNetworkCache(this)
            % getMexNetworkCache This is a private method that returns the
            % MexNetworkCache object attached to this network
            mexNetworkCache = this.MexNetworkCache;
        end
    end
    
    methods (Access = protected)
        function b = isQuantized(this)
            b = this.IsQuantized;
        end
    end

    methods
        % predict
        varargout = predict(this, X, varargin)
        
        % predictAndUpdateState
        varargout = predictAndUpdateState(this, X, varargin)
        
        % classify
        [labels, scores] = classify(this, X, varargin)
        
        % classifyAndUpdateState
        [this, labels, scores] = classifyAndUpdateState(this, X, varargin)
        
        % activations
        Y = activations(this, X, layerOut, varargin)
        
        % layerGraph
        lgraph = layerGraph(this)
        
        function this = resetState( this )
            % resetState   Reset the state of a recurrent neural network
            %
            %  net = resetState(net) returns the state of a recurrent
            %  neural network to an initial state of zeros.
            arguments
                this(1,1)
            end

            try
                this.PrivateNetwork = this.PrivateNetwork.resetNetworkState();
            catch err
                nnet.internal.cnn.util.rethrowDLExceptions(err);
            end
            
            % Instantiate new caches for the predict environment and layers
            % cache. This prevents caches being tied between the old (RHS)
            % and new (LHS) networks
            this.PredictEnvironment = nnet.internal.cnn.layer.learnable.CacheHandle();
            this.LayersCache = nnet.internal.cnn.layer.learnable.CacheHandle();
        end
        
        function plot(this)
            % plot   Plot a diagram of the DAG network
            %
            %   plot(net) plots a diagram of the DAG network net. Each
            %   layer in the diagram is labeled by its name.
            arguments
                this(1,1)
            end
            this.LayerGraph.plot();
        end
        
        function out = saveobj(this)
            out.Version = 2.0;
            out.Layers = this.Layers;
            out.Connections = this.Connections;
            out.SerializedQuantizationInfo = nnet.internal.cnn.layer.util.getQuantizationInfo(this.PrivateNetwork.OriginalLayers);
        end
    end
    
    methods(Static)
        function this = loadobj(in)
            layerQuantizationInfo = containers.Map();
            if in.Version >= 2 
                layerQuantizationInfo = in.SerializedQuantizationInfo.data;
            end
            this = DAGNetwork.makeNetwork(in.Layers, in.Connections, layerQuantizationInfo);
        end
    end
    
    methods(Static, Access = private)
        function this = makeNetwork(externalLayers, externalConnections, layerQuantizationInfo)
            % create a network using the external layers, external
            % connections and a map of the layer names with their
            % corresponding layer quantizationInfo
            % Get internal layers
            internalLayers = nnet.internal.cnn.layer.util.ExternalInternalConverter.getInternalLayers(externalLayers);
            
            % set the quantizationInfo on the internalLayers
            internalLayers = nnet.internal.cnn.layer.util.setQuantizationInfo(internalLayers, layerQuantizationInfo);
            % Get internal connections
            internalConnections = nnet.internal.cnn.util.externalToInternalConnections(externalConnections, internalLayers);
            % Create an internal layer graph
            internalLayerGraph = nnet.internal.cnn.LayerGraph(internalLayers, internalConnections);
            % Sort the internal layer graph
            [internalLayerGraph, topologicalOrder] = toposort(internalLayerGraph);
            % Create an internal DAG network
            internalDAGNetwork = nnet.internal.cnn.DAGNetwork(internalLayerGraph, topologicalOrder);
            % Create an external DAG network
            layersMap = nnet.internal.cnn.layer.util.InternalExternalMap(externalLayers);
            this = DAGNetwork(internalDAGNetwork, layersMap);
        end
        
        function dispatcher = createPredictionDataDispatcher(X, options, precision, netInfo)
            try
                hasResponses = false;
                options.Shuffle = 'never';
                options.DispatchInBackground = false;
                executionSettings.precision = precision;
                executionSettings.useParallel = false;
                numInputs = numel(netInfo.InputFormats);
                
                if numInputs == 1
                    % For single input, use the siso dispatcher factory
                    dispatcher = nnet.internal.cnn.dispatcher.DispatcherFactory.createDispatcher( ...
                        X, [], options, 'truncateLast', netInfo, executionSettings, hasResponses );
                    return;
                end

                % Multi Input
                if iIsCombinedDatastore(X) || iIsTransformedDatastore(X)
                    ds = X;                      
                elseif iscell(X)
                    % If X is a cell, it means the inputs for the
                    % network have been passed in as multiple in memory
                    % arrays (one for each input). Construct a combined
                    % datastore from this
                    ds = iConstructDatastoreFromMemoryArrays(X, netInfo, numInputs,  options.MiniBatchSize);
                else
                    error( message( 'nnet_cnn:DAGNetwork:InvalidPredictDataForMultipleInputNetwork', numInputs ) );
                end
                mapping = iConstructMapping(netInfo);
                sequenceOptions = iGetSequenceOptions(options);
                dispatcher = nnet.internal.cnn.dispatcher.DispatcherFactory.createDispatcherMIMO( ...
                    ds, mapping, [], options.MiniBatchSize, 'truncateLast', precision, executionSettings, options.Shuffle, [], sequenceOptions);
            catch e
                iThrowInvalidDataException( e, netInfo )
            end
        end
        
        function [options, executionEnvironment, acceleration, returnCategorical] = ...
                parseAndValidatePredictNameValuePairs(statefulPredict, varargin)
            
            keepUnmatched = false;
            inputArguments = nnet.internal.cnn.options.readNVPsWithOptionsDefinitionParser(...
                nnet.internal.cnn.options.InferenceOptionsDefinitionPredict(statefulPredict),...
                keepUnmatched,varargin{:});
            
            options = struct( ...
                'MiniBatchSize', inputArguments.MiniBatchSize, ...
                'SequenceLength', inputArguments.SequenceLength, ...
                'SequencePaddingValue', inputArguments.SequencePaddingValue, ...
                'SequencePaddingDirection', inputArguments.SequencePaddingDirection );
            executionEnvironment = inputArguments.ExecutionEnvironment;
            acceleration = inputArguments.Acceleration;
            returnCategorical = inputArguments.ReturnCategorical;
        end
        
        function [options, executionEnvironment, acceleration] = ...
                parseAndValidateClassifyNameValuePairs(statefulPredict, varargin)
            
            keepUnmatched = false;
            inputArguments = nnet.internal.cnn.options.readNVPsWithOptionsDefinitionParser(...
                nnet.internal.cnn.options.InferenceOptionsDefinition(statefulPredict),...
                keepUnmatched,varargin{:});
            
            options = struct( ...
                'MiniBatchSize', inputArguments.MiniBatchSize, ...
                'SequenceLength', inputArguments.SequenceLength, ...
                'SequencePaddingValue', inputArguments.SequencePaddingValue, ...
                'SequencePaddingDirection', inputArguments.SequencePaddingDirection);
            executionEnvironment = inputArguments.ExecutionEnvironment;
            acceleration = inputArguments.Acceleration;
        end
        
        function [options, outputAs, executionEnvironment, ...
                miniBatchesInCell, acceleration] = ...
                parseAndValidateActivationsNameValuePairs(varargin)
            
            keepUnmatched = false;
            inputArguments = nnet.internal.cnn.options.readNVPsWithOptionsDefinitionParser(...
                nnet.internal.cnn.options.InferenceOptionsDefinitionActivations,...
                keepUnmatched,varargin{:});
            
            options = struct( ...
                'MiniBatchSize', inputArguments.MiniBatchSize, ...
                'SequenceLength', inputArguments.SequenceLength, ...
                'SequencePaddingValue', inputArguments.SequencePaddingValue, ...
                'SequencePaddingDirection', inputArguments.SequencePaddingDirection);
            outputAs = inputArguments.OutputAs;
            executionEnvironment = inputArguments.ExecutionEnvironment;
            acceleration = inputArguments.Acceleration;
            miniBatchesInCell = inputArguments.MiniBatchesInCell;
        end
    end
    
    methods( Access = private )
        % predictStateful
        [Y, finalState, predictNetwork] = predictStateful(this, data, ...
            dispatcher, precision, predictNetwork, gpuShouldBeUsed)
        
        % calculatePredict
        Y = calculatePredict( ...
            this, X, options, executionEnvironment, acceleration )
        
        % calculateActivations
        Y = calculateActivations(this, X, layerIndex, layerOutputIndex, varargin)
        
        % predictBatch
        Y = predictBatch( this, X, options, executionEnvironment, precision, acceleration );
        
        function [predictNetwork, gpuShouldBeUsed, workerLoad, useParallel] = setupPredictionEnvironment(this, inferenceInfo)
            % If the cache is empty or the user is using another execution
            % environment, then fill the cache with the network set up
            % appropriately
            
            % This function is called from the entry-point inference
            % functions, and can throw legitimate setup errors. We use
            % try...catch to hide the internal call stack.
            try
                predictEnvironment = this.PredictEnvironment.Value;
                firstPrediction = this.PredictEnvironment.isEmpty;
                requestedExecutionEnvironment = inferenceInfo.ExecutionEnvironmentRequested;
                newEnvironment = firstPrediction || predictEnvironment.DeviceReset || ...
                    ~strcmp(predictEnvironment.ExecutionEnvironment, requestedExecutionEnvironment);
                
                workerLoad = [];
                if ismember( requestedExecutionEnvironment, {'multi-gpu', 'parallel'} )
                    % Stateful inference is not supported in parallel
                    if this.NetworkInfo.HasStatefulCustomLayer
                        error(message('nnet_cnn:DAGNetwork:InvalidEnvironmentForNetworkWithState', requestedExecutionEnvironment));
                    elseif this.NetworkInfo.NeedsStatefulPredict && inferenceInfo.RecurrentInferenceRequiresState
                        error(message('nnet_cnn:DAGNetwork:InvalidEnvironmentForRNN'));
                    end
                    executionSettings = deep.internal.parallel.setupExecutionEnvironment( ...
                        "ExecutionEnvironment",requestedExecutionEnvironment, ...
                        "DispatchInBackground",false,...
                        "WorkerLoad",workerLoad);
                    if executionSettings.IsThreadPool
                        error(message('nnet_cnn:internal:cnn:util:setupAndValidateParallel:ThreadsPoolNotSupported',requestedExecutionEnvironment))
                    end
                    useParallel = executionSettings.UseParallel;
                    workerLoad = executionSettings.WorkerLoad;
                else
                    useParallel = false;
                end

                if newEnvironment
                    gpuShouldBeUsed = nnet.internal.cnn.util.GPUShouldBeUsed(requestedExecutionEnvironment, workerLoad);
                else
                    gpuShouldBeUsed = predictEnvironment.GPUShouldBeUsed;
                end

                % Create an optimizer appropriate to the current settings.
                optimizer = nnet.internal.cnn.optimizer.NetworkOptimizationFactory.buildOptimizerFromParameters( ...
                    this, inferenceInfo, this.NetworkInfo, gpuShouldBeUsed, useParallel );
                
                % Do new setup if necessary
                newOptimization = firstPrediction || ~isequal(predictEnvironment.Optimization, optimizer.Identifier);
                if newEnvironment || newOptimization
                    
                    % verify that BatchNormalization layers have both the
                    % mean and the variance defined
                    iVerifyFinalizableLayers(this.PrivateNetwork.Layers(...
                        this.PrivateNetwork.FinalizableLayerIndices));
                    
                    % Optimize if necessary
                    if newOptimization
                        predictNetwork = this.PrivateNetwork.optimizeNetworkForPrediction(optimizer);
                    else
                        predictNetwork = predictEnvironment.PredictNetwork;
                    end
                    
                    % Set up for appropriate environment
                    if gpuShouldBeUsed
                        predictNetwork = predictNetwork.setupNetworkForGPUPrediction();
                    else
                        predictNetwork = predictNetwork.setupNetworkForHostPrediction();
                        % Invoke the size propagation API to ensure output sizes are known.
                        predictNetwork = predictNetwork.updateLayersAndOutputSizes();
                    end
                    
                    % Store settings for future comparison
                    predictEnvironment = struct( ...
                        'PredictNetwork', predictNetwork, ...
                        'ExecutionEnvironment', requestedExecutionEnvironment, ...
                        'GPUShouldBeUsed', gpuShouldBeUsed, ...
                        'Optimization', optimizer.Identifier, ...
                        'DeviceListener', [], ...
                        'DeviceReset', false);
                    
                    % If using the GPU, make sure that cached GPU data is
                    % refreshed if the device is reset by flagging the
                    % reset
                    if gpuShouldBeUsed
                        predictEnvironment.DeviceListener = event.listener( ...
                            parallel.gpu.GPUDeviceManager.instance, ...
                            "DeviceDeselecting", @(~,~)iFlagDeviceReset(this.PredictEnvironment));
                    end

                    this.PredictEnvironment.fillCache(predictEnvironment);
                else
                    predictEnvironment = this.PredictEnvironment.Value;
                    predictNetwork     = predictEnvironment.PredictNetwork;
                    gpuShouldBeUsed    = predictEnvironment.GPUShouldBeUsed;
                end
            catch ME
                throwAsCaller(ME);
            end
        end

        function Y = convertMultiOutputToCategorical(this, Y)
            privateNetwork = this.PrivateNetwork;
            numOutputs = privateNetwork.NumOutputs;
            for i = 1:numOutputs
                outputLayer = privateNetwork.OutputLayers{i};
                if iIsClassificationOutputLayer(outputLayer)
                    Y{i} = this.convertToCategorical(Y{i}, ...
                        privateNetwork.OutputLayerIndices(i), ...
                        outputLayer.Categories, ...
                        this.NetworkInfo.OutputFormats{i});
                end
            end
        end

        function Y = convertToCategorical(this, Y, outputLayerIdx, categories, outputFormat)
            if ~this.NetworkInfo.LayerHasSequenceOutput( outputLayerIdx )
                Y = iUndummify(Y, categories, outputFormat);
            else
                Y = iUndummifySequence(Y, categories);
            end
        end

    end
    
    % Custom Display
    methods (Hidden, Access = protected)

        function header = getHeader(this)
            header = getHeader@matlab.mixin.CustomDisplay(this);
            className = matlab.mixin.CustomDisplay.getClassNameForHeader(this);
            header = this.mixinQuantizedNetworkHeader(header, className);
        end

        function footer = getFooter(this)
            footer = getFooter@matlab.mixin.CustomDisplay(this);
            variableName = inputname(1);
            footer = this.mixinQuantizedNetworkFooter(footer, variableName);
        end

    end
end

function ds = iConstructDatastoreFromMemoryArrays(X, netInfo, numInputs,  miniBatchSize)
% Construct a combined datastore from all the in-memory arrays passed as
% input
inputDatastores = cell(1,numInputs);

% For each input, find the input information needed to construct the
% adequate in-memory array datastore
for idxInput = 1:numInputs
    currentInput = X{idxInput};
    currentInputFormat = netInfo.InputFormats{idxInput};

    if netInfo.IsFeatureInput(idxInput)
        % Data for feature input layers needs to be
        % transposed so the channel dimension is first.
        currentInput = iTable2ArrayAndTranspose(currentInput);
    end

    isSequenceInput = ~isempty(getSizeForDims(currentInputFormat,'T'));
    if isSequenceInput
        if iscell(currentInput)
            % Cell array data for sequence input layers needs to be
            % canonicalized since in SISO we support both Nx1 and 1xN cell
            % arrays, where N is the number of observations.
            currentInput = currentInput(:);
        else
            % Wrap single observation inputs in a
            % cell array.
            currentInput = { currentInput };
        end
        % For sequence data, observation dimension of the datastore is
        % along the rows, because each observation is in a cell
        datastoreObservationsDimension = 1;
    else
        % For non-sequence inputs, the data should be numeric. Find the
        % batch dimension in the input format
        datastoreObservationsDimension = finddim(currentInputFormat,'B');
    end
    inputDatastores{idxInput} = nnet.internal.cnn.datastore.InMemoryArrayDatastore( ...
        currentInput, datastoreObservationsDimension, miniBatchSize);
end
ds = combine(inputDatastores{:});
end

function exception = iCreateExceptionFromErrorID(errorID, varargin)
exception = MException(message(errorID, varargin{:}));
end

function iThrowInvalidDataException(exception, netInfo)
% iThrowInvalidDataException   Throws an InvalidData exception generated
% from DispatcherFactory depending on which type of network is being
% considered.
is3DNetwork = nnet.internal.cnn.util.is3DNetwork(netInfo.InputSizes{1});
if (strcmp(exception.identifier,'nnet_cnn:internal:cnn:DataDispatcherFactory:InvalidData'))
    if any(netInfo.HasSequenceInput)
        exception = iCreateExceptionFromErrorID('nnet_cnn:DAGNetwork:InvalidPredictDataForSequenceInputLayer');
    elseif any(netInfo.IsFeatureInput)
        exception = iCreateExceptionFromErrorID('nnet_cnn:DAGNetwork:InvalidPredictDataForFeatureInputLayer');
    elseif is3DNetwork
        exception = iCreateExceptionFromErrorID('nnet_cnn:DAGNetwork:InvalidPredictDataForImage3DInputLayer');
    else
        exception = iCreateExceptionFromErrorID('nnet_cnn:DAGNetwork:InvalidPredictDataForImageInputLayer');
    end
end
throwAsCaller(exception)
end

function mapping = iConstructMapping(netInfo)
% Construct mapping to pass information to DataDispatcherFactory (MIMO
% case) in a condensed way
numInputLayers = numel(netInfo.InputFormats);
mapping = cell(1,7);

% Set InputMapping and OutputMapping, i.e. indexes of input and output data
mapping{1} = num2cell(1:numInputLayers);
mapping{2} = {};
% Set AreClassificationOutputs, i.e. which output layers are classification layers.
mapping{3} = logical.empty();
% Set InputSizes and OutputSizes
mapping{4} = netInfo.InputSizes;
mapping{5} = [];
% Set InputFormats and OutputFormats
mapping{6} = netInfo.InputFormats;
mapping{7} = {};
end

function tf = iIsCombinedDatastore(x)
tf = isa(x, 'matlab.io.datastore.CombinedDatastore');
end

function tf = iIsTransformedDatastore(x)
tf = isa(x, 'matlab.io.datastore.TransformedDatastore');
end

function iVerifyFinalizableLayers(normalizationLayers)
for iFL=1:length(normalizationLayers)
    normalizationLayers{iFL}.checkPropertiesAreFinalized();
end
end

function tf = iCompare(net1, net2, fun, varargin)
% Compare net1 and net2.
tf = nnet.internal.cnn.util.compareNetworks(net1, net2, fun);
if nargin > 3
    % Deal with the case of more than two inputs by comparing net1
    % with each input separately.
    ii = 1;
    while tf && ii <= nargin-3
        tf = nnet.internal.cnn.util.compareNetworks(net1, varargin{ii}, fun);
        ii = ii+1;
    end
    return;
end
end

function X = iTable2ArrayAndTranspose(X)
if istable(X)
    X = table2array(X);
end
X = X';
end

function sequenceOptions = iGetSequenceOptions(options)
% iGetSequenceOptions   Get options that deal with sequences from the
% options that come from trainingOptions.
sequenceOptions = struct();
sequenceOptions.Length = options.SequenceLength;
sequenceOptions.PaddingValue = options.SequencePaddingValue;
sequenceOptions.PaddingDirection = options.SequencePaddingDirection;
end

function tf = iIsClassificationOutputLayer(internalLayer)
tf = isa(internalLayer, 'nnet.internal.cnn.layer.ClassificationLayer');
end

function labels = iUndummify(scores, classNames, outputFormat)
labels = nnet.internal.cnn.util.undummify( scores, classNames, outputFormat );
end

function labels = iUndummifySequence( scores, classNames )
isSingleObs = isnumeric( scores );
if isSingleObs
    scores = { scores };
end
labels = cell(numel(scores), 1);
for ii = 1:numel( scores )
    labels{ii} = nnet.internal.cnn.util.undummify( scores{ii}', classNames )';
end
if isSingleObs
    labels = labels{1};
end
end

function iFlagDeviceReset(predictEnvironment)
% Set the DeviceReset flag on the predict environment cache
env = predictEnvironment.Value;
env.DeviceReset = true;
predictEnvironment.fillCache(env);
end

function [layerOutputSize,layerOutputFormat] = iRemoveBTDimension(layerOutputSize,layerOutputFormat)
if endsWith(layerOutputFormat,'BT')
    layerOutputSize(end-1) = [];
    layerOutputFormat(end-1) = [];
end
end