classdef dlnetwork < nnet.cnn.network.HeterogeneousNetwork & matlab.mixin.CustomDisplay & ...
        deep.internal.quantization.QuantizedNetworkDisplay & ...
        deep.internal.container.ParameterContainer & ...
        nnet.cnn.network.AnalyzableNetwork
    %

    %   Copyright 2019-2024 The MathWorks, Inc.

    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
    end

    properties(Dependent)
        % Learnables    The network learnable parameters
        Learnables

        % State    The network state
        State
    end

    properties(Dependent, SetAccess=private)
        % InputNames   The names of the inputs in the network
        InputNames

        % Initialized   Value is true if the network is initialized
        Initialized
    end

    properties(Dependent)
        % OutputNames   The names of the outputs in the network
        OutputNames
    end

    properties(Hidden, Dependent, SetAccess=private)
        % TopologicalOrder   Topological order of the internal network
        TopologicalOrder
    end

    % Internal dlnetwork management
    properties(Dependent, Access={?nnet.internal.cnn.layer.CustomLayer,?nnet.internal.cnn.layer.util.CustomLayerInfo})
        % Internal dlnetwork. CustomLayer needs access because composition
        % allows CustomLayers to contain dlnetworks as Learnable
        % properties.
        PrivateNetwork
    end

    properties(Access=private)
        % Internal dlnetwork storage
        PrivateNetworkStorage

        % EvaluationStrategy   Strategy for optimization of network evaluation.
        EvaluationStrategy

        % PrototypeStrategy  Strategy for generation parameter prototype tables
        PrototypeStrategy
    end

    properties (Access=private)
        % ExampleInputs   Example input data used to initialize the
        %   dlnetwork, as deep.internal.PlaceholderArray objects
        ExampleInputs = {}

        % ExternalOutputNames  Stores custom OutputNames if specified by
        % the user. If is non-empty, we retrieve this value instead of the
        % default OutputNames (layers with unconnected output).
        ExternalOutputNames = [];

        % Unformatter  Removes formats from dlarrays and permutes to
        % preferred dimension layouts.
        Unformatter

        % LayerNames  Caches the layer names so they don't have to be retrieved
        % from the underlying network
        LayerNames
    end

    properties(Access=private, Transient)
        % CleanupToken  Used to trigger cleanup actions such as deleting
        % optimized network learnables
        CleanupToken = [];
    end

    properties(GetAccess=?nnet.internal.cnn.layer.CustomLayer)
        % InitializationState  "init", "uninit" or "auto"
        InitializationState(1,1) string {mustBeMember(InitializationState,["init","uninit","auto"])} = "uninit"
    end

    properties(Access=private, Dependent)
        % NumInputs   Number of inputs to the network
        NumInputs
    end

    properties(Hidden, SetAccess=private)
        % ConstructionID  Construction identification
        %   An integer representing the network identity (ignoring
        %   learnables or state). Constructing two networks with
        %   the same layerGraph will use distinct IDs. Modifying
        %   a network by setting learnables or state will share IDs.
        ConstructionID
    end

    methods
        function net = dlnetwork(lg, dlX, opts)
            arguments
                lg(:,1) = zeros(0,1);
            end
            arguments (Repeating)
                dlX {iValidateExampleInput(dlX)}
            end
            arguments
                opts.Initialize {iAssertBinary}
                opts.OutputNames {deep.internal.util.validateOutputNames(opts.OutputNames)}
            end

            isDefaultLayers = isequal(lg,zeros(0,1));
            isNoArgumentConstructor = isDefaultLayers && isempty(dlX) && isempty(fieldnames(opts));
            if isNoArgumentConstructor
                % User called dlnetwork() without arguments
                lg = nnet.cnn.layer.Layer.empty(0,1);
                opts.Initialize = false;
            elseif ~isfield(opts,'Initialize')
                % User called dlnetwork(___) without Initialize value. Set the default.
                opts.Initialize = true;
            end
            if ~isNoArgumentConstructor && isDefaultLayers
                % User called dlnetwork(___) without layers, but with some other inputs.
                error(message("nnet_cnn:dlnetwork:InvalidConstructorInput"));
            end

            if numel(dlX) > 0 && ~opts.Initialize
                error(message("nnet_cnn:dlnetwork:SpecifiedExampleInputsAndNoInitialize"));
            end
            if isfield(opts, 'OutputNames') && ~isempty(opts.OutputNames) && ~opts.Initialize
                % In order to specify custom OutputNames, the network needs
                % to be set to be initialized.
                error(message("nnet_cnn:dlnetwork:InvalidOutputNamesWithUninitialized"));
            end

            if (isa(lg, 'nnet.cnn.LayerGraph') && isscalar(lg)) || isa(lg, 'nnet.cnn.layer.Layer')
                try
                    privateNet = deep.internal.network.EditableNetwork(lg);
                    net.LayerNames = iGetInternalNetworkLayerNames(privateNet);
                    net.PrivateNetworkStorage = privateNet;
                catch me
                    throw(me)
                end
            else
                error(message('nnet_cnn:dlnetwork:InvalidConstructorInput'));
            end

            % Initialize if doing so during construction
            if opts.Initialize
                net = initialize(net, dlX{:});
            else
                % Set the prototype strategy
                net.PrototypeStrategy = deep.internal.network.util.EditablePrototypeStrategy();
            end

            if isfield(opts, 'OutputNames')
                % Validate OutputNames using internal dlnetwork utility
                stringOutputNames = string(opts.OutputNames);
                iWarnIfDuplicateOutputs(stringOutputNames);
                [~, ~] = getOutputLayersIndicesFromNames(net.PrivateNetwork, stringOutputNames);
                % Store OutputNames in cell arrays, to be consistent
                % with InputNames property.
                net.ExternalOutputNames = cellstr(stringOutputNames);
            end

            net = updateConstructionID(net);
        end

        % Getter and setter
        function val = get.Layers(this)
            val = getExternalLayers(this.PrivateNetwork);
        end

        function layer = getLayer(this, layerPath)
            arguments
                this
                layerPath(1,1) string
            end

            % Retrieve the top level layer
            tokens = split(layerPath,"/");
            parentLayerName = tokens(1);
            layerIdx = (this.LayerNames == parentLayerName);
            if ~any(layerIdx)
                error(message("nnet_cnn:dlnetwork:LayerDoesNotExist", parentLayerName));
            end
            layer = getExternalLayers(this.PrivateNetwork, layerIdx);

            % Deal with nested layer lookup
            if numel(tokens) > 1
                layer = getLayer(layer, join(tokens(2:end),"/"));
            end
        end

        function val = get.Connections(this)
            val = getExternalConnections(this.PrivateNetwork);
        end

        function lgraph = layerGraph(net)
            %layerGraph   Convert dlnetwork to layer graph
            %
            %  LGRAPH = layerGraph(NET) extracts a LayerGraph object
            %  from the dlnetwork object NET.
            arguments
                net(1,1)
            end
            % Ensure editable state
            editableNetwork = getEditableNetwork(net.PrivateNetwork);
            % Strip the meta-data to return a public LayerGraph object
            lgraph = toExternalLayerGraph(editableNetwork);
        end

        function plot(net)
            plot(layerGraph(net));
        end

        function net = removeLayers(net, layerNames)
            [net, editableNetwork] = makeEditable(net);
            try
                editableNetwork = removeLayers(editableNetwork, layerNames);
            catch err
                throw(err);
            end
            net = setEditableNetwork(net, editableNetwork);
            iWarnIfThereIsNoReturnArgument(nargout);
        end

        function net = connectLayers(net, s, d)
            [net, editableNetwork] = makeEditable(net);
            try
                editableNetwork = connectLayers(editableNetwork, s, d);
            catch err
                throw(err);
            end
            % Connecting won't modify the layers
            layersChanged = false;
            net = setEditableNetwork(net, editableNetwork, layersChanged);
            iWarnIfThereIsNoReturnArgument(nargout);
        end

        function net = disconnectLayers(net, s, d)
            [net, editableNetwork] = makeEditable(net);
            try
                editableNetwork = disconnectLayers(editableNetwork, s, d);
            catch err
                throw(err);
            end
            % Disconnecting won't modify the layers
            layersChanged = false;
            net = setEditableNetwork(net, editableNetwork, layersChanged);
            iWarnIfThereIsNoReturnArgument(nargout);
        end

        function net = addLayers(net, larray)
            [net, editableNetwork] = makeEditable(net);
            try
                editableNetwork = addLayers(editableNetwork, larray);
            catch err
                throw(err);
            end
            net = setEditableNetwork(net, editableNetwork);
            iWarnIfThereIsNoReturnArgument(nargout);
        end

        function net = addInputLayer(net, layer, inputName, opts)
            arguments
                net(1,1) dlnetwork
                layer(1,1) nnet.cnn.layer.Layer
                inputName {validateattributes(inputName, {'char','string','cell'},{'vector'})} = strings(1,0)
                opts.Initialize {nnet.internal.cnn.util.AutoTrueFalse.assertBinaryOrAuto} = "auto"
            end

            % Save the OutputNames as they can be edited by the user
            outputNames = string(net.OutputNames);

            [net, editableNetwork] = makeEditable(net);

            % Find the locations of the unconnected input layers that are
            % not InputLayers
            [inputLayerIdx, isInputLocationInputLayer] = findInputLayerIdx(editableNetwork);
            inputLocationNotInputLayer = inputLayerIdx(~isInputLocationInputLayer);
            unconnectedInputNameIdx = find(~isInputLocationInputLayer);
            networkInputNames = string(getExternalInputNames(editableNetwork));
            inputName = string(inputName);
            iValidateAddInputLayerInputs(layer, inputName, networkInputNames, unconnectedInputNameIdx);

            % Find the layer we'll connect to
            if isempty(inputName)
                % Pick the first unconnected input
                newInputLayerIndex = inputLocationNotInputLayer(1);
                inputName = networkInputNames(unconnectedInputNameIdx(1));
            else
                unconnectedNames = networkInputNames(~isInputLocationInputLayer);
                newInputLayerIndex = inputLocationNotInputLayer(inputName == unconnectedNames);
            end
            opts.Initialize = nnet.internal.cnn.util.AutoTrueFalse.convertToAutoTrueFalse(opts.Initialize);

            % Manipulate the graph.
            try
                % Add the input layer at the given index
                editableNetwork = addLayerAtIndex(editableNetwork, layer, newInputLayerIndex);
                % Connect the new layer to the selected input
                inputLayerName = editableNetwork.Layers(newInputLayerIndex).Name;
                editableNetwork = connectLayers(editableNetwork, inputLayerName, inputName);
            catch err
                % Refactor the exception to give a more specific error
                % message
                if err.identifier == "nnet_cnn:nnet:cnn:LayerGraph:LayerNamesMustNotAlreadyExist"
                    err = MException(message('nnet_cnn:dlnetwork:AddInputLayerExistingName'));
                end
                throw(err)
            end

            % Finish constructing a valid dlnetwork
            net = setEditableNetwork(net, matlab.lang.internal.move(editableNetwork));

            % Initialize if requested/possible
            allInputsConnected = isscalar(unconnectedInputNameIdx);
            net = iOptionallyInitialize(net, opts.Initialize, allInputsConnected, outputNames);
            iWarnIfThereIsNoReturnArgument(nargout);
        end

        function net = replaceLayer(net, layerName, larray, args)
            arguments
                net
                layerName {mustBeTextScalar}
                larray(:,1) nnet.cnn.layer.Layer
                args.ReconnectBy (1,1) string {mustBeMember(args.ReconnectBy, {'name', 'order'})} = 'name';
            end

            [net, editableNetwork] = makeEditable(net);
            try
                editableNetwork = replaceLayer(editableNetwork, string(layerName), larray, ReconnectBy=args.ReconnectBy);
            catch err
                throw(err);
            end
            net = setEditableNetwork(net, editableNetwork);
            iWarnIfThereIsNoReturnArgument(nargout);
        end

        function net = groupLayers(net, layerIds, opts)
            arguments
                net(1,1) dlnetwork
                layerIds(1,:) {iValidateLayerIds(net,layerIds)} = {}
                opts.GroupNames(1,:) string = string.empty
                opts.Delimiter(1,1) string {iValidateDelimiter}
                opts.Recursive(1,1) logical
            end
            % Validate the two modes: grouping by name (no IDs specified) or by ID
            groupingByPattern = isempty(layerIds);
            [delimiter, recursive] = iValidateGroupLayersOptions(groupingByPattern, opts);
            if groupingByPattern
                % Create regular expression for tokenizing with Delimiter
                % Escape every non alpha-numeric character in the delimiter in case it's
                % special
                layerIds = char("^(.+?)" + iEscapeText(delimiter) + "(.*)$");
            else
                if ~iscell(layerIds)
                    layerIds = {layerIds};
                end
                layerIds = cellfun(@(id)iConvertLayerIds(net,id), layerIds, UniformOutput=false);
            end
            iValidateLayerNames(opts.GroupNames, layerIds, groupingByPattern);

            populateFromInputLayers = true;
            exampleInputs = getExampleInputs(net, populateFromInputLayers);
            pnet = getPrivateNetwork(net);
            fuser = nnet.internal.cnn.optimizer.NetworkLayerFuser(layerIds, recursive, opts.GroupNames);
            [fusedNet, outputNames] = modify(pnet, {fuser}, exampleInputs, net.OutputNames);
            net = convertToExternal(fusedNet, exampleInputs);
            net.ExternalOutputNames = cellstr(outputNames);
        end

        function net = expandLayers(net, layerIds, opts)
            arguments
                net(1,1) dlnetwork
                layerIds(1,:) {iValidateLayerIds(net,layerIds)} = []
                opts.Delimiter(1,1) string {iValidateDelimiter} = ":"
                opts.Recursive(1,1) logical = true
            end
            layerIds = iConvertLayerIds(net,layerIds);
            iValidateNetworkLayers(net,layerIds);
            opts.RenamingSchema = "$n" + opts.Delimiter + "$l";

            exampleInputs = getExampleInputs(net);
            pnet = getPrivateNetwork(net);
            expander = nnet.internal.cnn.optimizer.NetworkLayerExpander(num2cell(layerIds), opts.Recursive, opts.RenamingSchema);
            [expandedNet, outputNames] = modify(pnet, {expander}, exampleInputs, net.OutputNames);
            net = convertToExternal(expandedNet, exampleInputs);
            net.ExternalOutputNames = cellstr(outputNames);
        end

        function learnables = get.Learnables(net)
            values = getLearnables(net.PrivateNetworkStorage);
            learnables = getLearnablesPrototype(net.PrototypeStrategy, net.PrivateNetworkStorage);
            if ~isempty(values)
                learnables.Value = values(:);
            end
        end

        function state = get.State(net)
            values = getState(net.PrivateNetworkStorage);
            state = stateValuesToTable(net, values);
        end

        function inputNames = get.InputNames(net)
            inputNames = getExternalInputNames(net.PrivateNetworkStorage);
        end

        function outputNames = get.OutputNames(net)
            if isempty(net.ExternalOutputNames)
                outputNames = getExternalOutputNames(net.PrivateNetworkStorage);
            else
                outputNames = net.ExternalOutputNames;
            end
        end

        function net = set.OutputNames(net,stringNames)
            if net.Initialized
                try
                    deep.internal.util.validateOutputNames(stringNames)
                    stringNames = string(stringNames);
                    iWarnIfDuplicateOutputs(stringNames)
                    % Validate OutputNames using internal dlnetwork utility
                    [~, ~] = getOutputLayersIndicesFromNames(net.PrivateNetwork, stringNames);
                    % Store OutputNames in cell arrays, to be consistent
                    % with InputNames property.
                    net.ExternalOutputNames = cellstr(stringNames);
                catch ME
                    throw(ME)
                end
            else
                error(message("nnet_cnn:dlnetwork:InvalidOutputNamesWithUninitialized"));
            end
        end

        function numInputs = get.NumInputs(net)
            numInputs = net.PrivateNetwork.NumInputs;
        end

        function net = set.State(net, state)
            iAssertValidState(state);

            % Assign to the storage property to avoid resetting the EvaluationStrategy
            if ~isempty(state)
                values = stateTableToValues(net, state);
                [net, pn] = net.prepareToUpdatePrivateNetwork();
                pn = setState(pn, values);
                isParameterUpdate = true;
                net = updatePrivateNetwork(net, pn, isParameterUpdate);
            end
        end

        function net = set.Learnables(net, learnables)
            iAssertValidLearnables(learnables);
            [net, pn] = net.prepareToUpdatePrivateNetwork();
            pn = setLearnables(pn, learnables.Value);
            isParameterUpdate = true;
            net = updatePrivateNetwork(net, pn, isParameterUpdate);
        end

        function net = setLearnRateFactor(net, varargin)
            narginchk(3,4);
            if ~isscalar(net)
                error(message('nnet_cnn:dlnetwork:NonScalarNetwork'));
            end
            [learnablesIndex, learnRateFactor] = iParseHyperParamArgs(net, varargin{:});
            learnableParameters = getLearnableParameters(net.PrivateNetwork);
            learnableParameters(learnablesIndex).LearnRateFactor = learnRateFactor;
            net.PrivateNetwork = setLearnableParameters(net.PrivateNetwork, learnableParameters);
        end

        function learnRateFactor = getLearnRateFactor(net, varargin)
            narginchk(2,3);
            if ~isscalar(net)
                error(message('nnet_cnn:dlnetwork:NonScalarNetwork'));
            end
            learnablesIndex = iParseHyperParamArgs(net, varargin{:});
            learnableParameters = getLearnableParameters(net.PrivateNetwork);
            learnRateFactor = learnableParameters(learnablesIndex).LearnRateFactor;
        end

        function net = setL2Factor(net, varargin)
            narginchk(3,4);
            if ~isscalar(net)
                error(message('nnet_cnn:dlnetwork:NonScalarNetwork'));
            end
            [learnablesIndex, l2Factor] = iParseHyperParamArgs(net, varargin{:});
            learnableParameters = getLearnableParameters(net.PrivateNetwork);
            learnableParameters(learnablesIndex).L2Factor = l2Factor;
            net.PrivateNetwork = setLearnableParameters(net.PrivateNetwork, learnableParameters);
        end

        function l2Factor = getL2Factor(net, varargin)
            narginchk(2,3);
            if ~isscalar(net)
                error(message('nnet_cnn:dlnetwork:NonScalarNetwork'));
            end
            learnablesIndex = iParseHyperParamArgs(net, varargin{:});
            learnableParameters = getLearnableParameters(net.PrivateNetwork);
            l2Factor = learnableParameters(learnablesIndex).L2Factor;
        end

        function privateNet = get.PrivateNetwork(net)
            privateNet = net.PrivateNetworkStorage;
        end

        function order = get.TopologicalOrder(this)
            % Get the order from the private network
            order = getTopologicalOrder(this.PrivateNetwork);
        end

        function net = set.PrivateNetwork(net, privateNet)
            onlyLearnables = false;
            net = updatePrivateNetwork(net, privateNet, onlyLearnables);
        end

        function net = initialize(net, dlX)
            arguments
                net(1,1)
            end
            arguments (Repeating)
                dlX {iValidateExampleInput(dlX)}
            end
            dlX = cellfun(@deep.internal.PlaceholderArray, dlX, 'Uniform', 0);

            try
                if net.InitializationState == "auto" && numel(dlX) == 0
                    % Retrieve initialized network directly
                    net = net.EvaluationStrategy.ExternalNetwork;
                else
                    % Finalize assembly
                    privateNetwork = net.PrivateNetwork;
                    net.PrivateNetworkStorage = [];
                    % Obtain an executable graph
                    net = finalizeInitialization(net, ...
                        getExecutableNetwork(matlab.lang.internal.move(privateNetwork), dlX), ...
                        dlX);
                end
            catch me
                nnet.internal.cnn.util.rethrowDLExceptions(me);
            end

            iWarnIfThereIsNoReturnArgument(nargout);
        end

        % Functional behavior
        function varargout = forward(net, x, NameValueArgs)
            arguments
                net(1,1)
            end
            arguments (Repeating)
                x
            end
            arguments
                NameValueArgs.Outputs { deep.internal.util.validateOutputNames(NameValueArgs.Outputs) }
                NameValueArgs.Acceleration(1,1) string { mustBeMember(NameValueArgs.Acceleration, ["none", "auto"]) } = "auto"
            end

            % Get the evaluation strategy.
            strategy = net.EvaluationStrategy;

            doForwardExampleInputs = validateAndPrepareForwardInputs(net.PrivateNetwork, x, "forward",[]);

            % Determine indices of the requested outputs
            [layerIndices, layerOutputIndices] = getExternalOutputLayersIndices(...
                net.PrivateNetwork, net.ExternalOutputNames, NameValueArgs);

            if net.InitializationState == "uninit"
                error(message('nnet_cnn:dlnetwork:DlnetworkUninitialized',"forward"));
            end

            nargoutchk(0, numel(layerIndices) + 1);
            try
                if doForwardExampleInputs
                    [varargout{1:nargout}] = forwardExampleInputs(net.PrivateNetwork, x, layerIndices, layerOutputIndices);
                else
                    % Edit Acceleration property
                    strategy.Acceleration = NameValueArgs.Acceleration;
                    % Call forward
                    [varargout{1:nargout}] = strategy.forward(net.PrivateNetwork, x, layerIndices, layerOutputIndices);
                    if nargout > numel(layerIndices)
                        varargout{nargout} = stateValuesToTable(net, varargout{nargout});
                    end
                end
            catch err
                nnet.internal.cnn.util.rethrowDLExceptions(err);
            end
        end

        function varargout = predict(net, x, NameValueArgs)
            arguments
                net(1,1)
            end
            arguments (Repeating)
                x
            end
            arguments
                NameValueArgs.Outputs { deep.internal.util.validateOutputNames(NameValueArgs.Outputs) }
                NameValueArgs.Acceleration(1,1) string { mustBeMember(NameValueArgs.Acceleration, ["none", "auto", "mex"]) } = "auto"
                NameValueArgs.InputDataFormats = "auto"
                NameValueArgs.OutputDataFormats = "auto"
            end

            % Get the evaluation strategy.
            strategy = net.EvaluationStrategy;

            inputFormats = deep.internal.util.validateDataFormats(NameValueArgs.InputDataFormats);
            outputFormats = deep.internal.util.validateDataFormats(NameValueArgs.OutputDataFormats);
            try
                [~,x,anyDlarrayInputs,anyFormattedDlarray] = validateAndPrepareForwardInputs(net.PrivateNetwork, x, "predict", inputFormats);

                % Determine indices of the requested outputs
                [layerIndices, layerOutputIndices] = getExternalOutputLayersIndices(...
                    net.PrivateNetwork, net.ExternalOutputNames, NameValueArgs);

                if net.InitializationState == "uninit"
                    error(message('nnet_cnn:dlnetwork:DlnetworkUninitialized',"predict"));
                end

                % Edit Acceleration property
                strategy.Acceleration = NameValueArgs.Acceleration;
                % Call predict
                nargoutchk(0, numel(layerIndices) + 1)
                [varargout{1:nargout}] = strategy.predict(net.PrivateNetwork, x, layerIndices, layerOutputIndices);

                % Determine number of data outputs. Create state table, if
                % state has been requested.
                numOutputs = numel(layerIndices);
                numOutputsReturned = length(varargout);
                if numOutputsReturned > numOutputs
                    varargout{end} = stateValuesToTable(net, varargout{end});
                    numDataOutputs = numOutputs;
                else
                    numDataOutputs = numOutputsReturned;
                end

                varargout = deep.internal.util.unformatOutputs(varargout, net, anyDlarrayInputs, anyFormattedDlarray,...
                                            inputFormats, outputFormats, numOutputs, numDataOutputs);
            catch err
                nnet.internal.cnn.util.rethrowDLExceptions(err);
            end
        end

        function net = resetState(net)
            arguments
                net(1,1)
            end
            % Check if we have states to reset
            if ~hasResettableState(net.PrivateNetworkStorage)
                return
            end
            try
                [net, pn] = net.prepareToUpdatePrivateNetwork();
                pn = resetState(pn);
                isParameterUpdate = true;
                net = updatePrivateNetwork(net, pn, isParameterUpdate);
            catch err
                nnet.internal.cnn.util.rethrowDLExceptions(err);
            end
        end

        function summary(net)
            % Collect the input layers from the private network
            [externalInputLayers, isInputLayer] = getExternalInputLayers(net.PrivateNetwork);
            % Generate all the summaries
            numInputs = numel(isInputLayer);
            summaries = strings(1,numInputs);
            for ii = 1:numInputs
                if isInputLayer(ii)
                    summaries(ii) = getInputLayerSummary(externalInputLayers{ii});
                end
            end
            networkData.Summaries = summaries;
            networkData.InputNames = net.InputNames;
            networkData.ExampleInputs = net.ExampleInputs;
            networkData.Initialized = net.Initialized;
            networkData.Learnables = getLearnables(net.PrivateNetworkStorage);
            lineSpacing = string(matlab.internal.display.lineSpacingCharacter);
            nnet.internal.cnn.dlnetwork.printNetworkSummary(networkData, lineSpacing);
        end

        function tf = get.Initialized(this)
            tf = this.InitializationState == "init";
        end

        function out = saveobj(this)
            out.Version = 5;
            out.LayerGraph = layerGraph(this);
            out.Initialized = this.Initialized;
            out.SerializedQuantizationInfo = nnet.internal.cnn.layer.util.getQuantizationInfo(getInternalLayers(this.PrivateNetwork));
            % Copy out size and format from example inputs so as not to
            % create a dependence on PlaceholderArray.
            fcn = @(plX)struct('Size',size(plX),'Format',dims(plX),'Value',plX.Value,'HasValue',plX.HasValue);
            out.ExampleInputs = cellfun(fcn, this.ExampleInputs, 'Uniform', 0);
            out.OutputNames = this.OutputNames;
        end
    end

    methods(Access=?deep.internal.SdkAccess)
        function [net, order] = toposort(net)
            % Sort the network topologically.
            privateNet = net.PrivateNetwork;
            net.PrivateNetworkStorage = [];
            [privateNet, order] = toposort(privateNet);
            net.PrivateNetwork = privateNet;
        end
    end

    methods(Static)
        function net = loadobj(in)
            try
                if in.Version == 1.0
                    net = dlnetwork(in.LayerGraph);
                else
                    exampleInputs = cellfun(@iBuildExampleInput, in.ExampleInputs, 'Uniform', 0);
                    if in.Initialized
                        % Use fast conversion because network has been
                        % already initialized, so it is definitely valid.
                        % NB: Do not force initialization to avoid casting
                        % learnables to single.
                        initializeNetworkWeights = false;
                        privateNet = deep.internal.network.EditableNetwork(in.LayerGraph);
                        net = convertToDlnetwork(privateNet, exampleInputs, initializeNetworkWeights);
                    else
                        net = dlnetwork(in.LayerGraph, exampleInputs{:}, Initialize=in.Initialized);
                    end
                end
            catch me
                causes = getReport(me);
                error(message('nnet_cnn:dlnetwork:LoadobjFailureCauses', causes));
            end

            if in.Version >= 3
                layerQuantizationInfo = in.SerializedQuantizationInfo.data;
                net = net.setQuantizationInfo(layerQuantizationInfo);
            end

            if in.Version >= 4
                if in.Initialized
                    net.OutputNames = in.OutputNames;
                end
            end
        end
    end

    methods(Hidden)
        function tf = isequal(this, net, varargin)
            % isequal   Check equality of dlnetwork objects
            %
            %   tf = isequal(net1,net2) checks equality of the dlnetwork
            %   objects net1 and net2. The function returns 1 (true) when
            %   the properties and architectures match and 0 (false)
            %   otherwise.
            %
            %   tf = isequal(net1,...,netN) checks equality of the N
            %   dlnetwork objects net1, …, netN.

            tf = iCompare(this, net, @isequal, varargin{:});
        end

        function tf = isequaln(this, net, varargin)
            % isequaln   Check equality of dlnetwork objects ignoring NaN values
            %
            %   tf = isequaln(net1,net2) checks equality of the dlnetwork
            %   objects net1 and net2, ignoring NaN values. The function
            %   returns 1 (true) when the properties and architectures
            %   match and 0 (false) otherwise.
            %
            %   tf = isequaln(net1,...,netN) checks equality of the N
            %   dlnetwork objects net1, …, netN, ignoring NaN values.

            tf = iCompare(this, net, @isequaln, varargin{:});
        end

        function privateNet = getPrivateNetwork(net)
            privateNet = net.PrivateNetwork;
        end

        function unformatter = getUnformatter(net)
            unformatter = net.Unformatter;
        end

        function strategy = getEvaluationStrategy(net)
            strategy = net.EvaluationStrategy;
        end

        function net = disableNetworkDelegates(net)
            net.EvaluationStrategy = nnet.internal.cnn.dlnetwork.PassthroughStrategy;
        end

        function net = resetNetworkDelegates(net)
            if net.Initialized
                net.EvaluationStrategy = nnet.internal.cnn.dlnetwork.DefaultOptimizationStrategy(net.PrivateNetworkStorage);
            end
        end

        function dlX = getExampleInputs(net, populateFromInputLayers)
            %getExampleInputs  Gives hidden access to the ExampleInputs property, and
            % can also be used with populateFromInputLayers=true to get example inputs
            % for networks that are being initialized from input layers.
            arguments
                net
                populateFromInputLayers = false
            end
            dlX = net.ExampleInputs;
            if populateFromInputLayers
                [layers, isInputLayer] = getInternalInputLayers(net.PrivateNetwork);
                inputIndex = 1;
                for l = 1:numel(layers)
                    thisLayer = layers{l};
                    n = max(1,numInputs(thisLayer));
                    if isInputLayer(l)
                        dlX(inputIndex:inputIndex+n-1) = iToCell(thisLayer.InputData);
                    end
                    inputIndex = inputIndex + n;
                end
            end
        end

        function net = setToAutoInitialize(net)
            net.EvaluationStrategy = nnet.internal.cnn.dlnetwork.AutoInitializationStrategy(net);
            net.InitializationState = "auto";
        end

        function varargout = classify(net, x) %#ok
            arguments
                net %#ok<INUSA>
            end
            arguments (Repeating)
                x %#ok<INUSA>
            end
            error(message('nnet_cnn:dlnetwork:ClassifyNotSupported'));
        end

    end

    % Implement ParameterContainer methods
    methods(Access={?deep.internal.container.Visitor,?deep.internal.container.Visitable})
        function this = traverseAccelerationParameters(this, visitor)
            if ~isscalar(this)
                error(message('nnet_cnn:dlnetwork:NonScalarNetwork'));
            end
            if ~this.Initialized
                return;
            end
            if ~isReadOnly(visitor)
                [this, pn] = this.prepareToUpdatePrivateNetwork();
            else
                pn = this.PrivateNetworkStorage;
            end
            learnables = getLearnables(pn);
            state = getState(pn);
            c = [learnables state];

            % We know that the parameters are a cell array of dlarrays, so jump
            % directly to the visitor method for this case
            c = visitDlarrayCell(visitor, c);
            
            if ~isReadOnly(visitor)
                pn = setLearnables(pn, c(1:length(learnables)));
                pn = setState(pn, c(length(learnables)+1:end));
                isParameterUpdate = true;
                this = this.updatePrivateNetwork(pn, isParameterUpdate);
            end
        end

        function this = traverseLearnableParameters(this, visitor)
            if ~isscalar(this)
                error(message('nnet_cnn:dlnetwork:NonScalarNetwork'));
            end
            if ~isReadOnly(visitor)
                [this, pn] = this.prepareToUpdatePrivateNetwork();
            else
                pn = this.PrivateNetworkStorage;
            end
            pn = visitLearnableLayers(pn, visitor);
            if ~isReadOnly(visitor)
                isParameterUpdate = true;
                this = this.updatePrivateNetwork(pn, isParameterUpdate);
            end
        end
    end
    methods( Access=protected )
        function key = getAccelerationCacheKey(this)
            key = this.ConstructionID;
        end

        function tf = canBeAccelerationOutput(~)
            tf = true;
        end
    end

    methods (Access = {?nnet.internal.cnn.layer.CustomLayer, ?deep.internal.train.network.DlnetworkAdapter})
        function net = aggregateState(net, numObs1, otherNet, numObs2)
            [net, pn] = net.prepareToUpdatePrivateNetwork();

            % Aggregate the internal networks
            pn = aggregateState(pn, numObs1, getPrivateNetwork(otherNet), numObs2);
            isParameterUpdate = true;
            net = updatePrivateNetwork(net, pn, isParameterUpdate);
        end
    end

    methods (Static, Access=?deep.internal.network.ExecutableNetwork)
        function net = createFromInternal(internalNet, exampleInput)
            % createFromInternal  Create an external dlnetwork from an
            % internal non-optimized dlnetwork, the internal to external
            % layer mapping and the topological order.
            arguments
                internalNet deep.internal.network.ExecutableNetwork
                exampleInput
            end
            % Create empty dlnetwork
            persistent emptyNet
            if isempty(emptyNet)
                emptyNet = dlnetwork(nnet.cnn.layer.Layer.empty(),'Initialize',0);
            end
            net = emptyNet;

            exampleInput = cellfun(@deep.internal.PlaceholderArray, exampleInput, 'Uniform', 0);
            net = finalizeInitialization(net, internalNet, exampleInput);

            % Update the network ID
            net = updateConstructionID(net);
        end
    end

    methods(Access = private)
        function [this, privateNet] = prepareToUpdatePrivateNetwork(this)
            %prepareToUpdatePrivateNetwork  Use before modifying the
            % private network parameters to ensure that an in-place update
            % is possible
            privateNet = this.PrivateNetwork;
            if this.Initialized
                this.PrivateNetworkStorage = [];
                if ~isempty(this.EvaluationStrategy)
                    strategy = this.EvaluationStrategy;
                    strategy.prepareForParameterUpdate();

                    % Any change to the parameters is tied to the
                    % lifetime of this network object, so make sure that we
                    % also prepare the evaluation strategy for another
                    % parameter update on deletion
                    this.CleanupToken = onCleanup(@()prepareForParameterUpdate(strategy));
                end
            end
        end

        function net = updatePrivateNetwork(net, privateNet, isParameterUpdate)
            % Update the private network.
            if net.Initialized
                % If initialized, we have to manage the evaluation strategy
                % creation or update
                if isempty(net.EvaluationStrategy)
                    net.EvaluationStrategy = nnet.internal.cnn.dlnetwork.DefaultOptimizationStrategy(privateNet);
                else
                    net.EvaluationStrategy = net.EvaluationStrategy.newStrategy(privateNet, isParameterUpdate);
                end
            end
            net.PrivateNetworkStorage = privateNet;
            if ~isParameterUpdate
                net.LayerNames = iGetInternalNetworkLayerNames(privateNet);
            end
        end

        function tbl = stateValuesToTable(net, values)
            % Return the state table for NET with given VALUES. ParameterMarker
            % entries are removed from the resulting table and the table is
            % shortened to only include the stateful layers that were
            % reached during propagation. values is a N-by-1 cell array of
            % state values for layers that were reached during propagation.
            tbl = getStatePrototype(net.PrototypeStrategy, net.PrivateNetworkStorage);
            tbl(numel(values)+1:end,:) = [];
            if ~isempty(values)
                mask = nnet.internal.cnn.layer.util.ParameterMarker.isMarker(values);
                tbl.Value = values(:);
                tbl(mask,:) = [];
            end
        end

        function [net, editableNetwork] = makeEditable(net)
            % Prepare the dlnetwork to be edited

            % Force the graph to be in its editable state
            editableNetwork = getEditableNetwork(net.PrivateNetwork);
            editableNetwork = makeStandard(editableNetwork);

            % Empty the storage to avoid double copy
            net.PrivateNetworkStorage = [];
            % Clear the optimizedNetwork
            net.EvaluationStrategy = [];
            % Mark as not initialized
            net.InitializationState = "uninit";
            % Clear the user set OutputNames
            net.ExternalOutputNames = [];
        end

        function net = setEditableNetwork(net, editableNetwork, layersChanged)
            arguments
                net
                editableNetwork
                layersChanged = true
            end
            % Post-process the dlnetwork after editing its layers or
            % connections

            % Assign the network to the storage
            net.PrivateNetwork = editableNetwork;

            net.ExampleInputs = {};

            % Set the prototype strategy for the new editable network
            net.PrototypeStrategy = deep.internal.network.util.EditablePrototypeStrategy();

            if layersChanged
                % Learnables and State may have changed, so reset the
                % construction ID
                net = updateConstructionID(net);
            end
        end

        function net = updateConstructionID(net)
            % Update the construction ID
            net.ConstructionID = nnet.internal.cnn.dlnetwork.cookie;
        end

        function net = finalizeInitialization(net, privateNet, dlX)
            % Save the example inputs used for this initialization
            net.ExampleInputs = dlX;
            % Mark the network as initialized
            net.InitializationState = "init";
            % Assign the private network. This will also create the
            % EvaluationStrategy.
            net.PrivateNetwork = privateNet;
            % Create and assign the unformatter object.
            net.Unformatter = iCreateUnformatter(privateNet);
            % Change the prototype strategy for the executable network
            net.PrototypeStrategy = deep.internal.network.util.ExecutablePrototypeStrategy(privateNet);
        end
    end

    methods(Hidden)
        function net = visitLearnableLayers(net, visitor)
            if ~isscalar(net)
                error(message('nnet_cnn:dlnetwork:NonScalarNetwork'));
            end
            [net, pn] = net.prepareToUpdatePrivateNetwork();
            pn = visitLearnableLayers(pn, visitor);
            isParameterUpdate = true;
            net = net.updatePrivateNetwork(pn, isParameterUpdate);
        end
        
        function net = setQuantizationInfo(net, layerQuantizationInfo)
            % Capture the private network and its internal layers
            hasQuantizedMetadata = deep.internal.quantization.isCompositeQuantizationEnabled(layerQuantizationInfo);
            if ~net.Initialized && hasQuantizedMetadata
                net = initialize(net);
            end

            privateNetwork = net.PrivateNetwork;
            net.PrivateNetworkStorage = [];
            internalLayers = getInternalLayers(privateNetwork);
            privateNetwork = clearInternalLayers(privateNetwork);
            
            
            if hasQuantizedMetadata
                % Set the QuantizationInfo on the internalLayers
                internalLayers = nnet.internal.cnn.layer.util.setQuantizationInfo(internalLayers, layerQuantizationInfo);
                % Make the private network quantization aware
                privateNetwork = makeQuantized(privateNetwork);
            else
                % Remove quantization info from the internal layers
                internalLayers = nnet.internal.cnn.layer.util.resetQuantizationInfo(internalLayers);
                % Make the private network standard
                privateNetwork = makeStandard(privateNetwork);
            end
            
            privateNetwork = updateInternalLayers(privateNetwork, internalLayers);
            net.PrivateNetwork = privateNetwork;
        end
    end

    methods (Access=?deep.internal.train.network.TrainableNetwork)
        function values = stateTableToValues(net, tbl)
            % Expand partial state table and validate names. Return the new table values
            % in the order defined by StatePrototype. Missing state is indicated by
            % a ParameterMarker.
            tbl = sortrows(tbl,1);
            tableLayerNames = tbl.Layer;
            tableParamNames = tbl.Parameter;
            tableValues = tbl.Value;
            tableLength = length(tableLayerNames);
            layerNames = unique(tableLayerNames);
            [prototype, numLayersWithState, prototypeSortOrder] = getStatePrototype(net.PrototypeStrategy, net.PrivateNetworkStorage);
            prototypeLayerNames = prototype.Layer;
            prototypeParamNames = prototype.Parameter;
            if numel(layerNames) > numLayersWithState
                error(message('nnet_cnn:dlnetwork:InvalidNumberOfStateLayers'))
            end
            assertLayerNamesExist(net.PrivateNetworkStorage, layerNames);
            numPrototypeStates = height(prototype);
            values = nnet.internal.cnn.layer.util.ParameterMarker.create(numPrototypeStates);
            prototypeMask = false(numPrototypeStates, 1);
            tblRow = 1;
            % Loop over input and prototype tables in sorted order
            for i = prototypeSortOrder
                if (tableLayerNames(tblRow) == prototypeLayerNames(i)) && ...
                        (tableParamNames(tblRow) == prototypeParamNames(i))
                    prototypeMask(i) = true;
                    values(i) = tableValues(tblRow);
                    tblRow = tblRow + 1;
                    if tblRow > tableLength
                        break;
                    end
                end
            end
            sortedParams = prototypeParamNames(prototypeSortOrder);
            sortedMask = prototypeMask(prototypeSortOrder);
            nnet.internal.cnn.layer.util.ParamTableCreator.validateParameterNames(tableParamNames, sortedParams(sortedMask));
        end
    end

    %% Friend methods that allow proxy to manipulate the payload
    methods(Access=?deep.internal.train.network.TrainableNetwork)
        function [net, learnableParameters, stateValues] = extractAndDeletePayload(net)
            % Delete the payload of the internal network and return the
            % extracted parameters. Assume the network is initialized

            % Clear the EvaluationStrategy and any optimizedNetwork
            % associated to it, to ensure no other copy of the learnables
            % exist
            net.EvaluationStrategy = [];

            % Capture parameters
            [net, pn] = net.prepareToUpdatePrivateNetwork();
            learnableParameters = getLearnableParameters(pn);
            stateValues = getState(pn);

            % Return learnable parameters as column cell array
            learnableParameters = num2cell(learnableParameters(:));
            % Return state as column array
            stateValues = stateValues(:);

            % Delete payload of the network
            pn = deletePayload(pn);

            % Update the private network with a deleted payload
            net = updatePrivateNetwork(net, pn, true);
        end

        function net = restorePayload(net, learnableParameters, stateValues)
            % Insert all parameters back in the internal network
            [net, pn] = net.prepareToUpdatePrivateNetwork();
            pn = restorePayload(pn, cat(2,learnableParameters{:}), stateValues);
            isParameterUpdate = true;
            net = updatePrivateNetwork(net, pn, isParameterUpdate);
        end
    end

    %% Friend methods that allow Custom Layers faster access for handling
    %  dlnetwork Learnables and State properties (nested networks)
    methods(Access=?nnet.internal.cnn.layer.CustomLayer)
        function net = setLearnableParameters(net, learnableParameters)
            %setLearnableParameters  Sets learnables skipping all
            % validation
            [net, pn] = net.prepareToUpdatePrivateNetwork();
            pn = setLearnableParameters(pn, learnableParameters);
            isParameterUpdate = true;
            net = updatePrivateNetwork(net, pn, isParameterUpdate);
        end

        function net = setDynamicParameters(net, dynamicParameters)
            %setDynamicParameters  Sets state skipping all validation
            [net, pn] = net.prepareToUpdatePrivateNetwork();
            pn = setDynamicParameters(pn, dynamicParameters);
            isParameterUpdate = true;
            net = updatePrivateNetwork(net, pn, isParameterUpdate);
        end

        function net = setLearnablesValues(net, learnablesValues)
            %setLearnablesValues  Sets learnables with layer validation but
            % without table validation
            [net, pn] = net.prepareToUpdatePrivateNetwork();
            pn = setLearnables(pn, learnablesValues);
            isParameterUpdate = true;
            net = updatePrivateNetwork(net, pn, isParameterUpdate);
        end
    end

    methods(Access={?nnet.internal.cnn.layer.CustomLayer, ?deep.internal.train.network.TrainableNetwork})
        function net = setStateValues(net, stateValues)
            %setStateValues  Sets state with layer validation but without
            % table validation
            [net, pn] = net.prepareToUpdatePrivateNetwork();
            pn = setState(pn, stateValues);
            isParameterUpdate = true;
            net = updatePrivateNetwork(net, pn, isParameterUpdate);
        end
    end

    methods(Access=?nnet.internal.cnn.analyzer.NetworkAnalyzer)
        function editableNetwork = getEditableNetwork(net)
            % Return the internal EditableNetwork object
            editableNetwork = getEditableNetwork(net.PrivateNetwork);
        end
    end

    methods (Access = protected)
        function b = isQuantized(this)
            b = this.PrivateNetwork.isQuantized();
        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 groups = getPropertyGroups(~)
            titleGroup = '';
            groups = matlab.mixin.util.PropertyGroup(iGetOrderedProperties(),titleGroup);
        end

        function footer = getFooter(this)
            link = nnet.internal.cnn.util.formHyperlink('helpPopup dlnetwork/summary', 'summary');
            footer = char("  " + getString(message('nnet_cnn:dlnetwork:FooterSummary',link)) + newline);
            variableName = inputname(1);
            footer = this.mixinQuantizedNetworkFooter(footer, variableName);
            
        end
    end

    methods (Hidden)
        function analyzerFactory = getAnalyzerFactory(~)
            analyzerFactory = deepapp.internal.analyzer.report.util.DlNetworkAnalyzerFactory();
        end
    end

end

function iValidateExampleInput(X)
if isa(X,'networkDataLayout')
    if isempty(X.Format)
        error(message('nnet_cnn:dlnetwork:InvalidExampleInputs'));
    end
    badDim = deep.internal.format.checkNetworkInitializationLayout(X.Size, X.Format);
    if ~isempty(badDim)
        error(message('nnet_cnn:dlnetwork:InvalidExampleInputsNaNLayout', badDim));
    end
elseif ~(isdlarray(X) || isa(X, 'deep.internal.PlaceholderArray')) || isempty(dims(X))
    error(message('nnet_cnn:dlnetwork:InvalidExampleInputs'));
end
end

function iAssertValidLearnables(learnables)
if ~nnet.internal.cnn.layer.util.ParamTableCreator.validateTableStructure(learnables) || ...
        ~iIsLearnablesValue(learnables.Value)
    error(message('nnet_cnn:dlnetwork:InvalidLearnables'))
end
end

function ok = iIsLearnablesValue(value)
% Test that all values are allowed dlarrays
ok = true;
for i = 1:numel(value)
    ok = iIsValidNumericLearnable(value{i});
    if ~ok
        break
    end
end
end

function iAssertValidState(state)
if ~nnet.internal.cnn.layer.util.ParamTableCreator.validateTableStructure(state) || ...
        ~iIsStatesValue(state.Value)
    error(message('nnet_cnn:dlnetwork:InvalidState'))
end
end

function tf = iIsValidNumericLearnable(value)
tf = nnet.internal.cnn.layer.paramvalidation.isValidLearnableValue(value);
end

function ok = iIsStatesValue(value)
% Test that values are either numeric or an unformatted dlarray
ok = true;
for i = 1:numel(value)
    ok = iIsValidNumericState(value{i});
    if ~ok
        break
    end
end
end

function tf = iIsValidNumericState(value)
tf = nnet.internal.cnn.layer.paramvalidation.isValidStateValue(value);
end

function iWarnIfDuplicateOutputs(names)
if numel(names) ~= numel(unique(names))
    warning(message('nnet_cnn:dlnetwork:DuplicateOutputs'));
end
end

function [learnablesIndex, paramValue] = iParseHyperParamArgs(net, ...
    layerName, paramName, paramValue)
%iParseHyperParamArgs  Generic utility for the hyper-parameter set/get
% functions to retrieve and validate the parameter

% Make sure the input layerName, paramName and paramValue are correct
isSetter = nargout == 2;
isParamPathIn = (isSetter && nargin < 4) || (~isSetter && nargin < 3);
if isParamPathIn
    if isSetter
        paramValue = paramName;
    end
    [layerName, paramName] = nnet.internal.cnn.util.parameterParts(layerName);
    if paramName == ""
        errorId = 'nnet_cnn:dlnetwork:ParameterNameNotSpecified';
        ME = MException(errorId, message(errorId));
        % Stack is clearer if this sub-function is not included
        throwAsCaller(ME);
    end
end

% Validate paramValue
if isSetter
    try
        nnet.internal.cnn.layer.paramvalidation.validateLearnFactor(paramValue);
    catch e
        % Stack is clearer if this sub-function is not included
        throwAsCaller(e)
    end
end

% Validate layer. First check via learnables for speed, but then
% double-check using all Layers before erroring.
learnables = net.Learnables;
layerMask = learnables.Layer == layerName;
if ~any(layerMask) && ~iLayerIsInNetwork(layerName,net)
    errorId = 'nnet_cnn:dlnetwork:LayerDoesNotExist';
    ME = MException(errorId, message(errorId, layerName));
    % Stack is clearer if this sub-function is not included
    throwAsCaller(ME);
end

% Validate learnable parameter against layer
learnablesMask = layerMask & learnables.Parameter == paramName;
if ~any(learnablesMask)
    errorId = 'nnet_cnn:internal:cnn:layer:Externalizable:ParameterNameNotFound';
    ME = MException(errorId, message(errorId, paramName));
    % Stack is clearer if this sub-function is not included
    throwAsCaller(ME);
end
learnablesIndex = find(learnablesMask,1);
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 iWarnIfThereIsNoReturnArgument(numArguments)
if numArguments == 0
    warning(message('nnet_cnn:dlnetwork:NoReturnArgument'));
end
end

function orderedProp = iGetOrderedProperties()
orderedProp = {'Layers', 'Connections', 'Learnables', 'State', 'InputNames', 'OutputNames', 'Initialized'};
end

function plX = iBuildExampleInput(sX)
%iBuildExampleInput  Helper to reconstruct example input from serialized
% struct
if ~isfield(sX,"HasValue") % Support for version 3
    sX.HasValue = ~(isscalar(sX.Value) && isnan(sX.Value));
end
if sX.HasValue
    plX = deep.internal.PlaceholderArray.buildWithValue(...
        sX.Size,sX.Format,sX.Value);
else
    plX = deep.internal.PlaceholderArray(sX.Size,sX.Format);
end
end

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

function tf = iLayerIsInNetwork(layerName, net)
tf = ismember(layerName, net.LayerNames);
end

function iValidateAddInputLayerInputs(layer, inputName, inputNames, unconnectedInputNameIdx)
internalLayer = nnet.internal.cnn.layer.util.ExternalInternalConverter.getInternalLayers(layer);
isInput = cellfun(@(layer)isa(layer,'nnet.internal.cnn.layer.InputLayer'),internalLayer);
if ~isInput
    error(message('nnet_cnn:dlnetwork:AddInputLayerInvalidLayer'));
end
nnet.internal.cnn.util.validateLayersForLayerGraph(layer);
if isempty(inputName)
    % Validate number of layers.
    if isempty(unconnectedInputNameIdx)
        error(message('nnet_cnn:dlnetwork:AddInputLayerNoUnconnectedInputs'));
    end
else
    % Validate manually specified targets to connect to.
    if ~isscalar(inputName) || ~any(ismember(inputName, inputNames(unconnectedInputNameIdx)))
        error(message('nnet_cnn:dlnetwork:AddInputLayerBadTarget'));
    end
end
end

function net = iOptionallyInitialize(net, initialize, allInputsConnected, outputNames)
if initialize == nnet.internal.cnn.util.AutoTrueFalse.Auto
    if allInputsConnected
        try
            net = iInitializeAndSetOutputNames(net, outputNames);
        catch
            % Do not report failed initialization. Users can query the Initialized property.
            % Do not warn either because user code may check Initialized, and the warning
            % would be counterproductive in that case.
        end
    end
elseif initialize == nnet.internal.cnn.util.AutoTrueFalse.True
    try
        net = iInitializeAndSetOutputNames(net, outputNames);
    catch err
        err = iTrimIntializeCause(err);
        throwAsCaller(err);
    end
end
end

function newErr = iTrimIntializeCause(err)
% Remove the cause which talks about the number of inputs to initialize.
% The user did not call initialize explicitly. Instead give all the other
% causes since those still apply.
initializeID = "nnet_cnn:cnn:analyzer:constraints:FunctionalConnections:NetworkInputsMismatchWithUnconnectedInputs";
causes = err.cause;
id = 'nnet_cnn:dlnetwork:AddInputLayerInitializeFailed';
newErr = MException(id,message(id));
for i = 1:length(causes)
    if err.cause{i}.identifier ~= initializeID
        newErr = addCause(newErr, causes{i});
    end
end
end

function net = iInitializeAndSetOutputNames(net, outputNames)
net = initialize(net);
net.OutputNames = outputNames;
end

function unformatter = iCreateUnformatter(net)
unformatter = deep.internal.getDefaultUnformatter;
[inputLayers, isInputLayer] = getInternalInputLayers(net);
numInputs = numel(isInputLayer);
fmts = cell(1,0);
for i = 1:numInputs
    % Only add default formats if the input is an input layer.
    if isInputLayer(i)
        fmt = getDefaultInputFormat(inputLayers{i});
        fmts{end+1} = fmt; %#ok<AGROW>
    end
end
unformatter = addFormats(unformatter,fmts);
end

function iValidateLayerIds(net,ids)
if iscell(ids)
    for i = 1:numel(ids)
        iValidateLayerIds(net,ids{i});
    end
else
    layerNames = net.LayerNames;
    if isnumeric(ids)
        mustBeInteger(ids);
        mustBeInRange(ids,1,numel(layerNames));
    else
        ids = string(ids);
        mustBeMember(ids,layerNames)
    end
end
end

function iValidateNetworkLayers(net,indicesOfNetworkLayers)
layers = getInternalLayersInUserOrder(net.PrivateNetwork);
netLayers = cellfun(@(l)isa(l,'nnet.internal.cnn.layer.Network'),layers(indicesOfNetworkLayers));
if ~all(netLayers)
    idx = find(~netLayers,1,'first');
    idx = indicesOfNetworkLayers(idx);
    error(message("nnet_cnn:dlnetwork:InvalidLayerID", idx, layers{idx}.Name));
end
end

function iValidateLayerNames(names, layerIndices, groupingByPattern)
if ~isempty(names)
    if groupingByPattern
        error(message("nnet_cnn:dlnetwork:NoGroupNamesWhenGroupingByPattern"));
    elseif numel(unique(names))~=numel(layerIndices)
        error(message("nnet_cnn:dlnetwork:InvalidGroupNames"));
    end
end
end

function [delimiter, recursive] = iValidateGroupLayersOptions(groupingByPattern, opts)
if ~isfield(opts, "Delimiter")
    delimiter = ":";
else
    if ~groupingByPattern
        error(message("nnet_cnn:dlnetwork:InvalidOptionsWhenGroupingByName"));
    end
    delimiter = opts.Delimiter;
end
if ~isfield(opts, "Recursive")
    recursive = true;
else
    if ~groupingByPattern
        error(message("nnet_cnn:dlnetwork:InvalidOptionsWhenGroupingByName"));
    end
    recursive = opts.Recursive;
end
end

function idx = iConvertLayerIds(net,ids)
if ischar(ids) || isstring(ids) || iscellstr(ids)
    [~,idx] = ismember(ids, net.LayerNames);
else
    idx = ids;
end
end

function text = iEscapeText(text)
%iEscapeText  Makes text safe for use in regexp
arguments
    text char
end
% Escape every character
escapedText = [repmat('\',1,numel(text)); text];
escapedText = escapedText(:);
% Remove the \ from in front of alphanumeric characters. This avoids
% creating new special characters such as "\n"
mask = find(contains(string(text'),alphanumericsPattern));
escapedText(2*mask-1) = [];
% Return as a string
text = string(escapedText');
end

function iValidateDelimiter(text)
assert(~contains(text,"/"), message("nnet_cnn:dlnetwork:InvalidDelimiter"));
end

function c = iToCell(x)
if ~iscell(x)
    c = {x};
end
end

function names = iGetInternalNetworkLayerNames(internalNetwork)
internalLayers = getInternalLayersInUserOrder(internalNetwork);
names = cellfun(@(l)l.Name, internalLayers, UniformOutput=false);
end
