classdef minibatchqueue < handle & matlab.mixin.Copyable ...
        & matlab.mixin.CustomDisplay
    %

    %   Copyright 2019-2023 The MathWorks, Inc.
    properties (Dependent, SetAccess = private)
        %MiniBatchSize - Size of the mini-batch
        %   Size of the mini-batch returned by next, specified as a
        %   positive integer. The default value is 128.
        %
        %   Read only.
        MiniBatchSize
    end
    
    properties (Dependent)
        %PartialMiniBatch - Return or discard incomplete mini-batches
        %	Indicates whether the mini-batches returned contain exactly
        %	MINIBATCHSIZE observations. Valid options are 'return' or
        %	'discard'. When set to 'return', the minibatchqueue returns all
        %	data even if some mini-batches do not contain exactly
        %	MINIBATCHSIZE observations. When set to 'discard', the
        %	minibatchqueue does not return the incomplete mini-batches. The
        %	default value is 'return'.
        PartialMiniBatch
    end
    
    properties (SetAccess = private)
        %MiniBatchFcn - Mini-batch preprocessing function
        %   Function used to preprocess and create mini-batches, specified
        %   as string, char array or function handle. The default value is
        %   'collate', which concatenates all variables to arrays. Custom
        %   functions must concatenate each mini-batch into an array after
        %   preprocessing and return each variable as a separate output.
        %   The function must accept at least as many inputs as the number
        %   of variables of the underlying datastore. The function can
        %   return as many variables as required. If the function specified
        %   by MINIBATCHFCN returns a different number of outputs than
        %   inputs, set the NUMOUTPUTS property to the number of outputs of
        %   the function.
        %
        %   Read only.
        MiniBatchFcn
        
        %NumOutputs - Number of mini-batch variables
        %   NUMOUTPUTS determines the number of outputs of the next
        %   function.
        %
        %   Read only.
        NumOutputs
        
        %OutputCast - Data type of each mini-batch variable
        %   Data type each mini-batch variable is converted to. Specify a
        %   value for each output. The default value is 'single'. Specify
        %   as empty to leave the output type unchanged.
        %
        %   Read only.
        OutputCast
        
        %OutputAsDlarray - Convert variable to dlarray
        %	Indicate whether to convert mini-batch variable to dlarray.
        %	Specify a logical value for each output. The default value is
        %	true. Variables converted to dlarray have underlying data type
        %	specified by OUTPUTCAST.
        %
        %   Read only.
        OutputAsDlarray
        
        %MiniBatchFormat - Format to apply to dlarray outputs
        %	Format to apply to dlarray outputs. The default value is ''.
        %	Specify a value for each output using a vector of strings or a
        %	cell array of character arrays. For dlarray outputs,
        %	MINIBATCHFORMAT can contain only 'S','C','B','T', or 'U', or be
        %	specified as empty. For nondlarray outputs, MINIBATCHFORMAT
        %	must be specified as empty.
        %
        %   Read only.
        MiniBatchFormat
    end
    
    properties
        %OutputEnvironment - Hardware environment for mini-batch variables
        %   Environment where each mini-batch variable will be provided
        %   using the next function. Valid options are 'auto', 'cpu' and
        %   'gpu'. The default value is 'auto'.
        %
        %   When set to 'cpu', the mini-batch variable is returned on the
        %   CPU. When set to 'gpu', the mini-batch variable is returned on
        %   the GPU. When set to 'auto', the function determines if a GPU
        %   is available and returns the mini-batch variable on the GPU if
        %   possible.
        OutputEnvironment
        
        %PreprocessingEnvironment - Environment for mini-batches preprocessing
        PreprocessingEnvironment
    end

    properties (Dependent)
        %DispatchInBackground - Use parallel pool to preprocess mini-batches
        %   Create and process mini-batches in the background, using
        %   workers in a parallel pool. The default value is false.
        %
        %   When set to true, minibatchqueue opens a local parallel pool
        %   using the current settings if a local pool is not currently
        %   open. Non-local pools are not supported. The workers prepare
        %   mini-batches in the background by applying the mini-batch
        %   function. Use this property to offload heavy preprocessing to a
        %   parallel pool. Further processing, including data type
        %   conversion, moving to GPU, or applying dlarray type format,
        %   does not happen on the workers.
        DispatchInBackground
    end
    
    properties (Access = private)
        %DispatcherState - State of the internal dispatcher
        %   The state object wrapping the internal dispatcher that provides
        %   the mini-batches to this object.
        DispatcherState
        
        %InternalMiniBatchFcn - Internal representation of MiniBatchFcn
        InternalMiniBatchFcn
        
        %ConversionOptions - Conversions options to use during next
        %   Structure containing all the conversion to be done, as indices.
        ConversionOptions
        
        %DatastoreMetadata - Supported APIs of the original datastore
        DatastoreMetadata
        
        %UseGPU - Internal flag indicating whether to use the GPU or not
        %   Internal logical vector. Initialized during the first call to
        %   next.
        UseGPU
    end
    
    properties(Constant, Hidden, Transient)
        %ClassVersion - Current version of the class used in saveobj
        ClassVersion = 3;
    end
    
    methods
        function this = minibatchqueue(inputDatastore, varargin)
            %MINIBATCHQUEUE  Construct a minibatchqueue object
            %
            %   MBQUEUE = MINIBATCHQUEUE(DATASTORE) creates a
            %   minibatchqueue from the input datastore.
            %
            % See also next, hasdata, reset
            
            narginchk(1,Inf);
            
            % Verify the input is a datastore
            if ~matlab.io.datastore.internal.shim.isDatastore(inputDatastore)
                error(message('deep:minibatchqueue:InputMustBeDatastore'))
            end
            originalDatastore = copy(inputDatastore);
            
            % Reset before preview
            reset(originalDatastore);
            numVariables = numel(getPreviewFromDatastore(originalDatastore));
            this.DatastoreMetadata = iGetDatastoreMetadata(originalDatastore);
            
            % Parse the inputs
            options = iParseInputs(numVariables, varargin{:});
            
            % Define the collating function for the internal dataloader
            collateFcn = @(x)collateMiniBatch(x,options.InternalMiniBatchFcn,...
                options.IsDefault.MiniBatchFcn,options.NumInputsMiniBatchFcn,options.NumOutputs);
            
            try
                % Create the internal dispatcher without using "this" to
                % avoid dependencies with the minibatchqueue object
                dispatcher = deep.internal.data.createDispatcher(...
                    originalDatastore, options.MiniBatchSize, collateFcn, ...
                    iReturnPartialMiniBatchValue(options.PartialMiniBatch));
                % Initialize the formatting options
                this.NumOutputs = options.NumOutputs;
                this.MiniBatchFcn = options.MiniBatchFcn;
                this.InternalMiniBatchFcn = options.InternalMiniBatchFcn;
                initializeOutputOptions(this, dispatcher, options.IsDefault, ...
                    options.OutputCast, options.OutputAsDlarray, ...
                    options.MiniBatchFormat);
                this.OutputEnvironment = options.OutputEnvironment;
                % Initialize the state of the dispatcher
                this.DispatcherState = deep.internal.data.BackgroundDispatcherDisabledState(dispatcher);
                % Update the PreprocessingEnvironment option
                this.PreprocessingEnvironment = options.PreprocessingEnvironment;
            catch me
                throw(me)
            end
        end
        
        function varargout = next(this)
            %NEXT  Returns the next mini-batch, with mini-batch function
            % and formatting applied.
            %
            %   [X1,...,XN] = NEXT(MBQUEUE);
            %
            % where N is the number of variables contained by the
            % minibatchqueue MBQUEUE. Use hasdata before calling NEXT to
            % verify that the minibatchqueue still contains mini-batches.
            %
            % See also hasdata, reset
            
            if nargout > this.NumOutputs
                error(message('deep:minibatchqueue:TooManyOutputsNext', this.NumOutputs))
            end

            if this.NumOutputs == 0
                % No outputs to return, so even if there is data, next
                % should error
                error(message('deep:minibatchqueue:NoMoreData'))
            end
            
            % Update the dispatcher state
            updateDispatcherState(this)
            
            % Get next mini-batch and post process it
            try
                [nextMiniBatch, nextMiniBatchSize] = next(this.DispatcherState.Dispatcher);
                if nextMiniBatchSize==0
                    % Dispatcher is empty
                    error(message('deep:minibatchqueue:NoMoreData'))
                end
                varargout = this.postProcessMiniBatch(matlab.lang.internal.move(nextMiniBatch), max(nargout,1));
            catch me
                throw(me)
            end
        end

        function reset(this)
            %RESET  Reset minibatchqueue.
            %
            %   RESET(MBQUEUE) resets the minibatchqueue to the start of
            %   the data. Use reset to obtain mini-batches from MBQUEUE
            %   after hasdata returns false.
            %
            % See also hasdata, next
            
            reset(this.DispatcherState.Dispatcher);
        end
        
        function shuffle(this)
            %SHUFFLE  Shuffle the observations held by the minibatchqueue.
            %
            %   SHUFFLE(MBQUEUE) randomly shuffles the mini-batches. The
            %   NEXT function then returns mini-batches in a different
            %   order than before shuffling. The function also resets
            %   MBQUEUE so all data in the minibatchqueue is shuffled. The
            %   underlying datastore must implement a shuffle method.
            %
            % See also hasdata, reset, next
            
            if ~this.DatastoreMetadata.IsShuffleable
                error(message('deep:minibatchqueue:ShuffleNotSupported'))
            end
            shuffle(this.DispatcherState.Dispatcher);
        end
        
        function tf = hasdata(this)
            %HASDATA  Returns true if any mini-batches are available.
            %
            % tf = HASDATA(MBQUEUE) returns true if a mini-batch can be
            % obtained from MBQUEUE using the next function. Use hasdata in
            % conjunction with next to iterate over all mini-batches in
            % MBQUEUE. If hasdata returns false, use reset or shuffle to
            % continue generating mini-batches.
            %
            % See also next, reset
            
            % Return if dispatcher can return more data or not. If
            % numOutputs is 0, no data can be returned
            tf = this.NumOutputs>0 && hasdata(this.DispatcherState.Dispatcher);
        end
        
        function obj = partition(this,n,index)
            %PARTITION  Returns a partitioned portion of the minibatchqueue
            %
            %   SUBMBQUEUE = PARTITION(MBQUEUE,NUMPARTITIONS,INDEX)
            %   partitions MBQUEUE into NUMPARTITIONS parts and returns the
            %   partitioned minibatchqueue, SUBMBQUEUE, corresponding to
            %   the index INDEX. The underlying datastore must implement a
            %   partition method.
            %
            % See also reset, next
            
            if ~this.DatastoreMetadata.IsPartitionable
                error(message('deep:minibatchqueue:PartitionNotSupported'))
            end
            % Extract the dispatcher to prevent double copy and preserve
            % the dispatcher state
            dispatcher = this.DispatcherState.Dispatcher;
            this.DispatcherState.Dispatcher = dispatcher.empty;
            cleanup = onCleanup(@()assignDispatcher(this,dispatcher));
            % Copy the object
            obj = copy(this);
            obj.assignDispatcher(partition(dispatcher,n,index));
        end
    end

    methods (Hidden)
        function varargout = read(~) %#ok<STOUT> 
            error(message('deep:minibatchqueue:UndefinedMethodRead'))
        end
    end

    methods (Access=?nnet.internal.cnn.dispatcher.minibatchqueueAdapter)
        function [data, miniBatchSize] = nextWithoutPostProcessing(this)
            % Return the next mini-batch without any post processing
            [data, miniBatchSize] = retrieveNextBatch(this, false);
        end
    end

    methods (Access=?deep.internal.train.Trainer)
        function [data, miniBatchSize] = retrieveNextBatch(this, postProcessing)
            % Return the next mini-batch in a cell format along with the
            % mini-batch size
            arguments
                this
                postProcessing = true
            end

            % Update the dispatcher state
            updateDispatcherState(this)

            % Get next mini-batch from the dispatcher
            [data, miniBatchSize] = next(this.DispatcherState.Dispatcher);
            if miniBatchSize == 0
                % Dispatcher cannot return more data
                error(message('deep:minibatchqueue:NoMoreData'))
            end
            if postProcessing
                numVariables = numel(data);
                data = this.postProcessMiniBatch(data, numVariables); 
            end
        end
    end
    
    methods (Access = protected)
        function obj = copyElement(this)
            %COPYELEMENT  Reimplement copyElement to allow for deep copy
            % of the internal datastore handle
            
            obj = copyElement@matlab.mixin.Copyable(this);

            try
                % Deep copy of the underlying dispatcher
                obj.DispatcherState.Dispatcher = copy(this.DispatcherState.Dispatcher);
            catch me
                throw(me)
            end
        end
    end
    
    methods (Access = private)
        function assignDispatcher(this, dispatcher)
            %ASSIGNDISPATCHER  Edit the dispatcher property with the given
            % dispatcher
            this.DispatcherState.Dispatcher = dispatcher;
        end
        
        function batch = postProcessMiniBatch(this, batch, numVariables)
            %POSTPROCESSMINIBATCH  Apply cast, dlarray and gpuArray
            % conversion to the mini-batch
            
            % Validate GPU selection
            if isempty(this.UseGPU)
                this.UseGPU = iGetInternalUseGPU(this.OutputEnvironment, this.NumOutputs);
            end
            
            for i = 1:numVariables
                batch{i} = convertOutputWithOptions(batch{i}, this.ConversionOptions(i), this.UseGPU(i), i);
            end
        end
        
        function updateDispatcherState(this)
            try
                this.DispatcherState = updateDispatcher(this.DispatcherState);
            catch me
                throwAsCaller(me)
            end
        end

        function initializeOutputOptions(this, datastoreDispatcher, isDefault, outputCast, outputAsDlarray, miniBatchFormat)
            %INITIALIZEOUTPUTOPTIONS  Initialize the output options to the
            % values specified as input using the original
            % DatastoreDispatcher object. The next batch is inspected to
            % assess the number of outputs is valid, and an initial post
            % processing is done to detect invalid formatting options.
            
            numOutputs = this.NumOutputs;
            [this.ConversionOptions, this.OutputCast, this.OutputAsDlarray, this.MiniBatchFormat] = ...
                iInitializeConversionOptions(numOutputs, isDefault, outputCast, outputAsDlarray, miniBatchFormat);
            
            if ~hasdata(datastoreDispatcher)
                % No data at start. This means the datastore is not empty
                % but removes all the observations. Give up on validating
                % the output options
                return
            end

            % Collect the next mini-batch that will be returned by the
            % dispatcher
            batch = preview(datastoreDispatcher);

            % Validate the output options by attempting to convert the
            % first mini-batch
            for i = 1:numOutputs
                convertOutputWithOptions(batch{i}, this.ConversionOptions(i), false, i);
            end
        end
        
        function enableBackgroundDispatch(this, useBackgroundPool)
            % Change the dispatcher state to use background dispatch

            if ~this.DatastoreMetadata.IsPartitionable
                % Note: a subsettable datastore is partitionable, thus
                % partitionable is the minimum condition
                error(message('deep:minibatchqueue:DispatchInBackgroundInputNotSupported'));
            end
            this.DispatcherState = enableBackgroundDispatch(this.DispatcherState, useBackgroundPool);
        end
        
        function disableBackgroundDispatch(this)
            % Change the dispatcher state to disable background dispatch
            this.DispatcherState = disableBackgroundDispatch(this.DispatcherState);
        end
    end
    
    methods (Access=?deep.internal.minibatchqueue.MinibatchqueueAccess)
        function obj = subset(this,indices)
            %SUBSET  Returns a subset of the minibatchqueue.
            %
            %   SUBMBQUEUE = SUBSET(MBQUEUE,INDICES) returns the subset
            %   minibatchqueue, SUBMBQUEUE, corresponding to the indices
            %   INDICES of the underlying datastore. The underlying
            %   datastore must implement a subset method.
            %
            % See also reset, next, partition
            
            if ~this.DatastoreMetadata.IsSubsettable
                error(message('deep:minibatchqueue:SubsetNotSupported'))
            end
            % Extract the dispatcher to prevent double copy and preserve
            % the dispatcher state
            dispatcher = this.DispatcherState.Dispatcher;
            this.DispatcherState.Dispatcher = dispatcher.empty;
            cleanup = onCleanup(@()assignDispatcher(this,dispatcher));
            % Copy the object
            obj = copy(this);
            obj.assignDispatcher(subset(dispatcher,indices));
        end

        function n = numpartitions(this)
            %NUMPARTITIONS  Returns the maximum number of partitions of the
            %minibatchqueue.
            %
            %   N = NUMPARTITIONS(MBQUEUE) returns the maximum number of
            %   partitions of the minibatchqueue. The underlying datastore
            %   must implement a partition method.
            %
            % See also partition, subset

            if ~this.DatastoreMetadata.IsPartitionable
                % Note: a subsettable datastore is partitionable, thus
                % partitionable is the minimum condition
                error(message('deep:minibatchqueue:NumPartitionsNotSupported'));
            end
            n = numpartitions(this.DispatcherState.Dispatcher);
        end

        function tf = isPartitionable(this)
            %ISPARTITIONABLE  Returns true if the minibatchqueue supports
            %partition.
            %
            %   TF = ISPARTITIONABLE(MBQUEUE) returns true if the
            %   minibatchqueue supports partition. The underlying datastore
            %   must implement a partition method.
            %
            % See also partition, numpartitions, subset

            tf = this.DatastoreMetadata.IsPartitionable;
        end

        function tf = isSubsettable(this)
            %ISSUBSETTABLE  Returns true if the minibatchqueue supports
            %subset.
            %
            %   TF = ISSUBSETTABLE(MBQUEUE) returns true if the
            %   minibatchqueue supports subset. The underlying datastore
            %   must implement a subset method.
            %
            % See also subset, numpartitions, partition

            tf = this.DatastoreMetadata.IsSubsettable;
        end

        function tf = isShuffleable(this)
            %ISSHUFFLABLE  Returns true if the minibatchqueue supports
            %shuffle.
            %
            %   TF = ISSHUFFLABLE(MBQUEUE) returns true if the
            %   minibatchqueue supports shuffle. The underlying datastore
            %   must implement a shuffle method.
            %
            % See also shuffle

            tf = this.DatastoreMetadata.IsShuffleable;
        end

        function setMiniBatchFormat(this, miniBatchFormat)
            numFormats = numel(miniBatchFormat);
            assert(numFormats == this.NumOutputs, 'Number of MiniBatchFormats must match NumOutputs')

            % OutputAsDlarray always true for any outputs with formats
            outputAsDlarray = true(1, numFormats);
            
            % Update the properties
            % NB: Responsibility is on the caller to check that
            % miniBatchFormat is valid
            this.MiniBatchFormat = miniBatchFormat;
            this.OutputAsDlarray = outputAsDlarray;
            this.ConversionOptions = iCreateConversionOptions(this.OutputCast, outputAsDlarray, miniBatchFormat);
        end
    end

    methods (Access=?deep.internal.SdkAccess)
        function setMiniBatchSize(this, miniBatchSize)
            iAssertValidMiniBatchSize(miniBatchSize)
            this.DispatcherState.Dispatcher.MiniBatchSize = miniBatchSize;
        end
    end
    
    methods 
        function minibatchsize = get.MiniBatchSize(this)
            minibatchsize = this.DispatcherState.Dispatcher.MiniBatchSize;
        end
        
        function set.PartialMiniBatch(this,newValue)
            iAssertNonEmptyString(newValue);
            newValue = iValidatePartialMiniBatchValue(newValue);
            this.DispatcherState.Dispatcher.ReturnIncompleteMiniBatch = iReturnPartialMiniBatchValue(newValue);
        end
        
        function partialMiniBatch = get.PartialMiniBatch(this)
            partialMiniBatch = iGetPartialMiniBatch(this.DispatcherState.Dispatcher.ReturnIncompleteMiniBatch);
        end
        
        function set.OutputEnvironment(this, newValue)
            iAssertValidCharStringOrCellStr(newValue);
            if ~isempty(newValue)
                newValue = iValidateOutputEnvironment(newValue);
            end
            this.OutputEnvironment = ...
                iValidateOptionSize("OutputEnvironment", newValue, this.NumOutputs, false); %#ok<MCSUP>
            % Clear the existing value for UseGPU so that future "next"
            % call updates the value
            this.UseGPU = []; %#ok<MCSUP>
        end
        
        function set.DispatchInBackground(this, newValue)
            arguments
                this
                newValue (1,1) {iAssertValidDispatchInBackground(newValue)}
            end
            if newValue
                this.PreprocessingEnvironment = 'parallel';
            else
                this.PreprocessingEnvironment = 'serial';
            end
        end

        function tf = get.DispatchInBackground(this)
            tf = this.PreprocessingEnvironment ~= "serial";
        end

        function set.PreprocessingEnvironment(this, newValue)
            iAssertNonEmptyString(newValue);
            newValue = iValidatePreprocessingEnvironmentValue(newValue);
            this.PreprocessingEnvironment = newValue;
            % Modify the state of the dispatcher
            if newValue == "serial"
                disableBackgroundDispatch(this);
            else
                enableBackgroundDispatch(this, newValue=="background");
            end
        end
    end
    
    % Custom Display methods
    methods (Access = protected)
        function header = getHeader(obj)
            %GETHEADER  Modify the header to include the number of outputs
            if ~isscalar(obj)
                header = getHeader@matlab.mixin.CustomDisplay(obj);
            else
                if obj.NumOutputs==0
                    outputStr = 'no output';
                elseif obj.NumOutputs==1
                    outputStr = '1 output';
                else
                    outputStr = [num2str(obj.NumOutputs) ' outputs'];
                end
                headerStr = matlab.mixin.CustomDisplay.getClassNameForHeader(obj);
                headerStr = [headerStr,' with ',outputStr, ' and properties:'];
                header = sprintf('%s\n',headerStr);
            end
        end
        function propgrp = getPropertyGroups(obj)
            %GETPROPERTYGROUPS  Custom grouping of the properties
            if ~isscalar(obj)
                propgrp = getPropertyGroups@matlab.mixin.CustomDisplay(obj);
            else
                propListMiniBatch = struct(...
                    'MiniBatchSize',obj.MiniBatchSize,...
                    'PartialMiniBatch',obj.PartialMiniBatch, ...
                    'MiniBatchFcn',obj.MiniBatchFcn, ...
                    'PreprocessingEnvironment',obj.PreprocessingEnvironment);
                propListOutputs = struct( ...
                    'OutputCast',{obj.OutputCast},...
                    'OutputAsDlarray',{obj.OutputAsDlarray},...
                    'MiniBatchFormat',{obj.MiniBatchFormat},...
                    'OutputEnvironment',{obj.OutputEnvironment});
                
                propgrpMiniBatch = matlab.mixin.util.PropertyGroup(...
                    propListMiniBatch, "Mini-batch creation:");
                propgrpOutputs = matlab.mixin.util.PropertyGroup(...
                    propListOutputs, "Outputs:");
                
                propgrp = [propgrpMiniBatch propgrpOutputs];
            end
        end
    end
    
    % save/load methods
    methods(Hidden)
        function savedata = saveobj(this)
            % Save the user defined properties
            savedata.NumOutputs = this.NumOutputs;
            savedata.MiniBatchSize = this.MiniBatchSize;
            savedata.PartialMiniBatch = this.PartialMiniBatch;
            savedata.MiniBatchFcn = this.MiniBatchFcn;
            savedata.OutputCast = this.OutputCast;
            savedata.OutputAsDlarray = this.OutputAsDlarray;
            savedata.MiniBatchFormat = this.MiniBatchFormat;
            savedata.OutputEnvironment = this.OutputEnvironment;
            savedata.PreprocessingEnvironment = this.PreprocessingEnvironment;
            % Save DispatchInBackground for forward compatibilty
            savedata.DispatchInBackground = this.PreprocessingEnvironment ~= "serial";
            % Save a copy of the datastore held internally
            savedata.OriginalDatastore = getDatastore(this.DispatcherState.Dispatcher);
            % Save version number
            savedata.ClassVersion = minibatchqueue.ClassVersion;
            % Save the minimum version number required for the object to be
            % loaded.
            savedata.MinimumSupportedVersion = 1;
        end
    end
    methods(Static, Hidden)
        function obj = loadobj(savedata)
            if isstruct(savedata)
                % Verify if the saved minibatchqueue is from a later,
                % unsupported version
                if savedata.ClassVersion > minibatchqueue.ClassVersion && ...
                        minibatchqueue.ClassVersion < savedata.MinimumSupportedVersion
                    id = 'deep:minibatchqueue:UnsupportedVersion';
                    msg = getString(message(id));
                    loadWarningString = getString(message('MATLAB:load:classError', 'minibatchqueue', msg));
                    warning(id, loadWarningString);
                    % Return an empty minibatchqueue
                    obj = minibatchqueue(arrayDatastore([]));
                    return
                end

                if savedata.ClassVersion < 3
                    % Save struct is coming from an older release which
                    % does not have PreprocessingEnvironment
                    if savedata.DispatchInBackground
                        savedata.PreprocessingEnvironment = "parallel";
                    else
                        savedata.PreprocessingEnvironment = "serial";
                    end
                end
                
                % Create the minibatchqueue with the saved options
                obj = minibatchqueue(savedata.OriginalDatastore, ...
                    savedata.NumOutputs, ...
                    "MiniBatchSize", savedata.MiniBatchSize, ...
                    "PartialMiniBatch", savedata.PartialMiniBatch, ...
                    "MiniBatchFcn", savedata.MiniBatchFcn, ...
                    "OutputCast", savedata.OutputCast, ...
                    "OutputAsDlarray", savedata.OutputAsDlarray, ...
                    "MiniBatchFormat", savedata.MiniBatchFormat, ...
                    "OutputEnvironment", savedata.OutputEnvironment, ...
                    "PreprocessingEnvironment", savedata.PreprocessingEnvironment ...
                    );
            else
                % The saved data was already a minibatchqueue
                obj = savedata;
            end
        end
    end
end


function options = iParseInputs(numVariables, varargin)
%IPARSEINPUTS  Handle the inputs of the minibatchqueue constructor and return
% a valid datastore and options struct, where invalid option combinations
% have been checked.

% Parse the NVPs to get a struct of options with valid values.
options = iParseNameValuePairs(varargin{:});

% Get the default value of the number of outputs if none specified
if isempty(options.NumOutputs)
    options.NumOutputs = numVariables;
end

% Validate the MiniBatchFcn and retrieve the function to use
[options.MiniBatchFcn, options.InternalMiniBatchFcn, options.NumInputsMiniBatchFcn] = ...
    iValidateMiniBatchFcn(options.MiniBatchFcn, numVariables, options.NumOutputs);

% Modify the default OutputEnvironment property with the number of outputs
options.OutputEnvironment = iInitializeDefaultOutputEnvironment(...
    options.OutputEnvironment, options.IsDefault.OutputEnvironment, options.NumOutputs);
end


function options = iParseNameValuePairs(varargin)
%IPARSENAMEVALUEPAIRS  Parse the NVP during construction. Returns an
% "options" struct that contains valid fields and values, without checking
% incompatible options.

% Create the parser and default
parser = inputParser();
defaultNumOutputs = [];
defaultMiniBatchSize = 128;
defaultOutputEnvironment = 'auto';
defaultPartialMiniBatch = 'return';
defaultOutputCast = 'single';
defaultOutputAsDlarray = true;
defaultMiniBatchFormat = '';
defaultMiniBatchFcn = 'collate';
defaultDispatchInBackground = false;
defaultPreprocessingEnvironment = 'serial';

% Add the parameters
parser.addOptional('NumOutputs',defaultNumOutputs, ...
    @(x)iValidateNonDlarrayAttributes(x, {'numeric'}, {'integer','nonnegative'}));
parser.addParameter('MiniBatchSize',defaultMiniBatchSize, ...
    @iAssertValidMiniBatchSize);
parser.addParameter('OutputEnvironment',defaultOutputEnvironment, ...
    @iAssertValidCharStringOrCellStr);
parser.addParameter('PreprocessingEnvironment',defaultPreprocessingEnvironment, ...
    @iAssertValidCharStringOrCellStr);
parser.addParameter('DispatchInBackground',defaultDispatchInBackground, ...
    @iAssertValidDispatchInBackground);
parser.addParameter('MiniBatchFcn',defaultMiniBatchFcn, ...
    @iAssertValidMiniBatchFcnValue);
parser.addParameter('PartialMiniBatch',defaultPartialMiniBatch, ...
    @iAssertNonEmptyString);
parser.addParameter('OutputCast',defaultOutputCast, ...
    @iAssertValidCharStringOrCellStr);
parser.addParameter('OutputAsDlarray',defaultOutputAsDlarray, ...
    @(x)iValidateNonDlarrayAttributes(x, {'logical','numeric'}, {'vector','binary'}));
parser.addParameter('MiniBatchFormat',defaultMiniBatchFormat, ...
    @iAssertValidCharStringOrCellStr);

% Parse the inputs
parser.parse(varargin{:});
options = parser.Results;

% Convert logical options
options.DispatchInBackground = logical(options.DispatchInBackground);
options.OutputAsDlarray = logical(options.OutputAsDlarray);

% Return valid values for the string options
options.PartialMiniBatch = iValidatePartialMiniBatchValue(options.PartialMiniBatch);
options.OutputCast = cellstr(options.OutputCast);
options.MiniBatchFormat = cellstr(options.MiniBatchFormat);

options.IsDefault.OutputEnvironment = ismember('OutputEnvironment',parser.UsingDefaults);
options.IsDefault.MiniBatchFormat = ismember('MiniBatchFormat',parser.UsingDefaults);
options.IsDefault.OutputAsDlarray = ismember('OutputAsDlarray',parser.UsingDefaults);
options.IsDefault.MiniBatchFcn = ismember('MiniBatchFcn',parser.UsingDefaults);
options.IsDefault.OutputCast = ismember('OutputCast',parser.UsingDefaults);

% Manage DispatchInBackground which is an alias of PreprocessingEnvironment
if iNeedToUseDispatchInBackgroundValue(parser)
    % Convert the DispatchInBackground option
    if options.DispatchInBackground
        options.PreprocessingEnvironment = 'parallel';
    else
        options.PreprocessingEnvironment = 'serial';
    end
end
end

function tf = iNeedToUseDispatchInBackgroundValue(parser)
% Use DispatchInBackground if it was specified and
% PreprocessingEnvironment was not
tf = ~ismember('DispatchInBackground',parser.UsingDefaults) && ismember('PreprocessingEnvironment',parser.UsingDefaults);
end

function iAssertValidMiniBatchSize(value)
iValidateNonDlarrayAttributes(value, {'numeric'}, {'scalar','integer','positive'});
end

function iAssertValidDispatchInBackground(value)
iValidateNonDlarrayAttributes(value, {'logical','numeric'}, {'scalar','binary'});
end

function value = iValidatePartialMiniBatchValue(value)
%IVALIDATEPARTIALMINIBATCHVALUE  Return a valid value to the
% PartialMiniBatch property
supportedPartialMiniBatch = {'return','discard'};
value = char(validatestring(value, supportedPartialMiniBatch, '', 'PartialMiniBatch'));
end

function value = iValidatePreprocessingEnvironmentValue(value)
supportedPreprocessingEnvironment = {'serial','background','parallel'};
value = char(validatestring(value, supportedPreprocessingEnvironment, '', 'PreprocessingEnvironment'));
end

function tf = iReturnPartialMiniBatchValue(value)
%IRETURNPARTIALMINIBATCH  Return true if partial mini-batches should be
% returned
tf = strcmp(value,"return");
end

function partialMiniBatch = iGetPartialMiniBatch(returnIncompleteMiniBatch)
%IGETPARTIALMINIBATCH  Return the displayed string corresponding to the
% PartialMiniBatch option
if returnIncompleteMiniBatch
    partialMiniBatch = 'return';
else
    partialMiniBatch = 'discard';
end
end

function value = iValidateOutputEnvironment(value)
%IVALIDATEOUTPUTENVIRONMENT  Return a valid value for the OutputEnvironment
%property.
supportedOutputEnvironment = {'auto','gpu','cpu'};
value = cellstr(value);

for i = 1:numel(value)
    value{i} = char(validatestring(value{i}, supportedOutputEnvironment, '', "OutputEnvironment"));
end
end

function [miniBatchFcn, internalMiniBatchFcn, numInputsMiniBatchFcn] = ...
    iValidateMiniBatchFcn(miniBatchFcn, numVariables, numOutputs)
%IVALIDATEMINIBATCHFCN  Ensure that the MiniBatchFcn provided exists and
% has the expected number of inputs.
if isa(miniBatchFcn, 'function_handle')
    try
        numInputsMiniBatchFcn = nargin(miniBatchFcn);
        numOutputsMiniBatchFcn = nargout(miniBatchFcn);
    catch me
        if me.identifier == "MATLAB:narginout:functionDoesnotExist"
            error(message('deep:minibatchqueue:MiniBatchFcnUndefined',func2str(miniBatchFcn)))
        else
            rethrow(me)
        end
    end
    
    % The MiniBatchFcn accepts too many inputs
    if (numInputsMiniBatchFcn > numVariables) && (numInputsMiniBatchFcn ~= -1)
        error(message('deep:minibatchqueue:MiniBatchFcnMismatchingNumInputs', numVariables))
    end
    % Too many NumOutputs compared to the number of outputs of the
    % MiniBatchFcn. Some outputs would be unassigned.
    if (numOutputs > numOutputsMiniBatchFcn) && (numOutputsMiniBatchFcn ~= -1)
        error(message("deep:minibatchqueue:MiniBatchFcnMismatchNumOutputs", numOutputs, numOutputsMiniBatchFcn));
    end
    
    internalMiniBatchFcn = miniBatchFcn;
else
    % Validate the user input
    supportedFcnMap = iGetSupportedMiniBatchFcn();
    miniBatchFcn = char(validatestring(miniBatchFcn, supportedFcnMap.keys(), '', 'MiniBatchFcn'));
    internalMiniBatchFcn = supportedFcnMap(miniBatchFcn);
    
    % When using the default MiniBatchFcn, numOutputs cannot exceed the
    % number of variables in the datastore
    if numOutputs > numVariables
        error(message("deep:minibatchqueue:InvalidNumOutputs", numOutputs, numVariables))
    end
    numInputsMiniBatchFcn = numVariables;
end
end

function supportedFcnMap = iGetSupportedMiniBatchFcn()
%IGETSUPPORTEDMINIBATCHFCN  Return a map containing the supported
%MiniBatchFcn values and their associated function.
persistent fcnMap
if isempty(fcnMap)
    miniBatchFcnKey = {'collate'};
    miniBatchFcnVal = {@collate};
    fcnMap = containers.Map(miniBatchFcnKey, miniBatchFcnVal);
end
supportedFcnMap = fcnMap;
end

function iAssertValidMiniBatchFcnValue(value)
%IASSERTVALIDMINIBATCHFCNVALUE  Assert the input is a non-empty function
% handle or string.
validateattributes(value, {'char','string','function_handle'}, {'nonempty'});
if isa(value,'function_handle')
    validateattributes(value, {'function_handle'}, {'scalar'});
end
end

function iAssertValidCharStringOrCellStr(value)
%IASSERTVALIDCHARSTRINGORCELLSTR  Assert the input is a valid char, string
% or cellstr
if ~(ischar(value) && isempty(value)) % Allow an empty char
    validateattributes(value, {'char','string', 'cell'},{'row'});
end
if iscell(value) && ~iscellstr(value) %#ok<ISCLSTR>
    error(message("deep:minibatchqueue:OptionInvalidCellStr"));
end
end

function optionValue = iValidateOptionSize(optionName, optionValue, numVariables, isDefault)
%IVALIDATEOPTIONSIZE  Validate the option has enough values, or expand it
% if only one value was specified

numSpecifiedOptions = numel(optionValue);
invalidNumberOfOptions = numSpecifiedOptions ~= numVariables && numSpecifiedOptions ~= 1;
specifiedNonEmptyWithNoOutputs = ~isDefault && (numSpecifiedOptions ~= 0) && (numVariables == 0);
if invalidNumberOfOptions || specifiedNonEmptyWithNoOutputs
    messageId = "deep:minibatchqueue:" +optionName+ "InvalidSize";
    error(message(messageId, numVariables));
end
% Repeat the cellstr if only one value
if numSpecifiedOptions==1 && numVariables~=1
    optionValue = repmat(optionValue, 1, numVariables);
end
end

function iAssertNonEmptyString(value)
%IASSERTNONEMPTYSTRINGWITHSUPPORTEDVALUES  Assert the input is non-empty
validateattributes(value, {'char','string'}, {'nonempty'});
end

function datastoreMetadata = iGetDatastoreMetadata(datastore)
% Return logical flags indicating if shuffle and partition are supported by
% the input datastore
datastoreMetadata.IsShuffleable = isShuffleable(datastore);
datastoreMetadata.IsPartitionable = isPartitionable(datastore);
datastoreMetadata.IsSubsettable = isSubsettable(datastore);
end

function internalUseGPU = iGetInternalUseGPU(outputIsOnGPU, numOutputs)
%IGETINTERNALUSEGPU  Compute the internal UseGPU. Replace 'auto' by the gpu
%availability, and error if true is selected without an available GPU.
persistent gpuAvailable
if isempty(gpuAvailable)
    gpuAvailable = canUseGPU;
end

internalUseGPU = false(1,numOutputs);
for i = 1:numOutputs
    if strcmp(outputIsOnGPU{i},"auto")
        internalUseGPU(i) = gpuAvailable;
    else
        internalUseGPU(i) = strcmp(outputIsOnGPU{i},"gpu");
    end
    if internalUseGPU(i) && ~gpuAvailable
        error(message('deep:minibatchqueue:NoGPUAvailable', i))
    end
end
end

function [conversionOptions, outputCast, outputAsDlarray, miniBatchFormat] = ...
    iInitializeConversionOptions(numOutputVariables, isDefault, outputCast, outputAsDlarray, miniBatchFormat)
%IINITIALIZECONVERSIONOPTIONS  Initialize the values of the formatting
% options OutputCast, OutputAsDlarray and MiniBatchFormat with the number of
% output variables. Expand the values if only one value was specified.

outputAsDlarray = iValidateOptionSize("OutputAsDlarray", outputAsDlarray, numOutputVariables, isDefault.OutputAsDlarray);
outputCast = iValidateOptionSize("OutputCast", outputCast, numOutputVariables, isDefault.OutputCast);
miniBatchFormat = iValidateOptionSize("MiniBatchFormat", miniBatchFormat, numOutputVariables, isDefault.MiniBatchFormat);
conversionOptions = iCreateConversionOptions(outputCast, outputAsDlarray, miniBatchFormat);

% Prevent setting a MiniBatchFormat value if OutputAsDlarray is false
nonDlarrayOutputAndFormatNonEmpty = ~outputAsDlarray & ~cellfun(@isempty, miniBatchFormat);
if any(nonDlarrayOutputAndFormatNonEmpty)
    idx = find(nonDlarrayOutputAndFormatNonEmpty, 1, 'first');
    error(message('deep:minibatchqueue:MiniBatchFormatSpecifiedForNonDlarray', idx))
end
end

function outputEnvironment = iInitializeDefaultOutputEnvironment(outputEnvironment, isDefault, numOutputs)
% iInitializeDefaultOutputEnvironment  Initialize the default
% OutputEnvironment to a valid value for the number of outputs
if isDefault && (numOutputs == 0)
    % With 0 outputs, replace the default OutputEnvironment with an empty
    % cell array
    outputEnvironment = cell(1,0);
end
end

function iValidateNonDlarrayAttributes(x, expectedClasses, expectedAttributes)
deep.internal.dlarray.mustBeNonDlarray(x);
validateattributes(x, expectedClasses, expectedAttributes)
end

function conversionOptions = iCreateConversionOptions(outputCast, outputAsDlarray, miniBatchFormat)
conversionOptions = struct('Cast', outputCast, ...
    'ConvertToDlarray', num2cell(outputAsDlarray), ...
    'MiniBatchFormat', miniBatchFormat);
end