classdef (Hidden) LabeledIOModel < InputOutputModel
   %   This class manages the metadata of input/output models.

   %   Author(s): P. Gahinet
   %   Copyright 2021 The MathWorks, Inc.

   % Public properties with restricted value (dependent to avoid issues
   % with data validation at load time)
   properties (Access = public, Dependent)
      % Input channel names (cell array of char vectors, default = '' for all
      % channels).
      %
      % Set this property to a string for single-input models, for example,
      % 'torque'. For multi-input models, use a string vector to name each
      % input channel, for example, {'thrust' ; 'aileron deflection'}. You
      % can also use a single string to label the input channels as entries
      % of a vector-valued signal. For example, the string 'v' is interpreted
      % as {'v(1)' ; 'v(2)' ; 'v(3)'} for a model with three inputs. Use the
      % empty string '' for unnamed input channels.
      %
      % Note that sys.u is shorthand for sys.InputName.
      InputName
      % Input channel units (cell array of char vectors, default = '' for all
      % channels).
      %
      % Use this property to keep track of input signal units. You can set
      % the InputUnit property to:
      %  * A string for single-input models, for example, 'kg'
      %  * A string vector for multi-input models, for example, {'m/s';'Nm'}
      InputUnit
      % Input channel groups (structure, default = struct with no field).
      %
      % The InputGroup property partitions the input channels of MIMO
      % systems into groups and lets you refer to each group by name. Input
      % groups are specified as a structure whose field names are the group
      % names and whose field values are the input channels belonging to
      % each group. For example,
      %    sys.InputGroup.controls = [1 2]
      %    sys.InputGroup.noise = [3 5]
      % creates two input groups labeled "controls" and "noise" that include
      % input channels 1,2 and 3,5, respectively. You can then extract the
      % transfer function from the "controls" inputs to all outputs using
      %    sys(:,'controls')
      InputGroup
      % Output channel names (cell array of char vectors, default = '' for all
      % channels).
      %
      % Set this property to a string for single-output models, for example,
      % 'power'. For multi-output models, use a string vector to name each
      % output channel, for example, {'altitude' ; 'pitch angle'}. You can
      % also use a single string to label the output channels as entries of
      % a vector-valued signal. For example, the string 'z' is interpreted
      % as {'z(1)' ; 'z(2)'} for a model with two outputs. Use the empty
      % string '' for unnamed output channels.
      %
      % Note that sys.y is shorthand for sys.OutputName.
      OutputName
      % Output channel units (cell array of char vectors, default = '' for all
      % channels).
      %
      % Counterpart of InputUnit property for output channels. Type
      % "help DynamicSystem.InputUnit" for details.
      OutputUnit
      % Output channel groups (structure, default = struct with no field).
      %
      % Counterpart of InputGroup property for output channels. Type
      % "help DynamicSystem.InputGroup" for details.
      OutputGroup
      % User notes (string vector, default: 0x1 string array).
      %
      % This property can store any text, for example,
      %    sys.Notes = "This model was created on Jan. 1st, 2000".
      Notes
   end

   % Public properties with arbitrary value
   properties (Access = public)
      % Additional information or data about the system (default = []).
      %
      % This property can store any MATLAB data type.
      UserData;
   end

   properties (Hidden, Dependent)
      % Input names. Alias for "InputName" property.
      u
      % Output names. Alias for "OutputName" property.
      y
   end

   % Private properties (raw data, not get/set methods). Methods should
   % interact with these properties rather than their public counterparts
   properties (Access = protected)
      % String vector, Nu-by-1 or 0-by-1 to mean ["";...;""]
      InputName_ = strings(0,1);
      % String vector, Nu-by-1 or 0-by-1 to mean ["";...;""]
      InputUnit_ = strings(0,1);
      % Structure
      InputGroup_ = struct;
      % String vector, Ny-by-1 or 0-by-1 to mean ["";...;""]
      OutputName_ = strings(0,1);
      % String vector, Ny-by-1 or 0-by-1 to mean ["";...;""]
      OutputUnit_ = strings(0,1);
      % Structure
      OutputGroup_ = struct;
      % String vector. Can also store cell array of char vectors for
      % backward compatibility.
      Notes_ = strings(0,1);
   end

   properties (Access = protected, Transient)
      % Setting CrossValidation=false defers all consistency checks in
      % the SET methods for public properties. Other types of data validation
      % (e.g., enforcing the correct datatype) are not affected. This flag
      % is needed to support multi-property SET operations.
      CrossValidation_ = true;
   end

   % PUBLIC METHODS
   methods

      function Value = get.InputName(sys)
         % GET function for InputName property
         Value = cellstr(getInputName_(sys));
      end

      function Value = get.OutputName(sys)
         % GET function for OutputName property
         Value = cellstr(getOutputName_(sys));
      end

      function Value = get.u(sys)
         % u is shorthand for InputName
         Value = cellstr(getInputName_(sys));
      end

      function Value = get.y(sys)
         % y is shorthand for OutputName
         Value = cellstr(getOutputName_(sys));
      end

      function sys = set.InputName(sys,Value)
         % SET function for InputName property
         sys = setInputName_(sys, Value);
      end

      function sys = set.u(sys,Value)
         sys.InputName = Value;
      end

      function sys = set.OutputName(sys,Value)
         % SET function for OutputName property
         sys = setOutputName_(sys, Value);
      end

      function sys = set.y(sys,Value)
         sys.OutputName = Value;
      end

      function Value = get.InputUnit(sys)
         % GET function for InputUnit property
         Value = cellstr(ltipack.fullstring(sys.InputUnit_,sys.IOSize_(2)));
      end

      function Value = get.OutputUnit(sys)
         % GET function for OutputUnit property
         Value = cellstr(ltipack.fullstring(sys.OutputUnit_,sys.IOSize_(1)));
      end

      function sys = set.InputUnit(sys,Value)
         % SET function for InputUnit property
         sys.InputUnit_ = ltipack.mustBeStringVector(Value,'InputUnit',false);
         if sys.CrossValidation_
            sys = checkInputConsistency(sys);
         end
      end

      function sys = set.OutputUnit(sys,Value)
         % SET function for OutputUnit property
         sys.OutputUnit_ = ltipack.mustBeStringVector(Value,'OutputUnit',false);
         if sys.CrossValidation_
            sys = checkOutputConsistency(sys);
         end
      end

      function Value = get.InputGroup(sys)
         % GET function for InputGroup property
         Value = sys.InputGroup_;
      end

      function Value = get.OutputGroup(sys)
         % GET function for OutputGroup property
         Value = sys.OutputGroup_;
      end

      function sys = set.InputGroup(sys,Value)
         % SET function for InputGroup property
         if isempty(Value)
            Value = struct;
         elseif isstruct(Value)
            % Remove empty groups
            Value = localRemoveEmptyGroups(Value);
         end
         sys.InputGroup_ = Value;
         if sys.CrossValidation_
            sys = checkInputGroup(sys);
         end
      end

      function sys = set.OutputGroup(sys,Value)
         % SET function for OutputGroup property
         if isempty(Value)
            Value = struct;
         elseif isstruct(Value)
            % Remove empty groups
            Value = localRemoveEmptyGroups(Value);
         end
         sys.OutputGroup_ = Value;
         if sys.CrossValidation_
            sys = checkOutputGroup(sys);
         end
      end

      function Value = get.Notes(sys)
         % GET function for Notes property
         Value = sys.Notes_;
      end

      function sys = set.Notes(sys,Value)
         % SET function for Notes property
         if isempty(Value)
            sys.Notes_ = strings(0,1);
         else
            if ~isstring(Value)
               % Accept char or cellstr for backward compatibility
               try
                  Value = cellstr(Value);
               catch
                  error(message('Control:ltiobject:setLTI4','Notes'))
               end
            end
            sys.Notes_ = Value(:);
         end
      end

   end

   %% METADATA HANDLING IN BINARY OPERATIONS AND TRANSFORMATIONS
   methods (Access = protected)

      %------------
      function sys = copyMetaData(refsys,sys)
         % Fast data copy from one object to another
         sys.InputName_ = refsys.InputName_;
         sys.OutputName_ = refsys.OutputName_;
         sys.InputUnit_ = refsys.InputUnit_;
         sys.OutputUnit_ = refsys.OutputUnit_;
         sys.InputGroup_ = refsys.InputGroup_;
         sys.OutputGroup_ = refsys.OutputGroup_;
         sys.Name_ = refsys.Name_;
         sys.Notes_ = refsys.Notes_;
         sys.UserData = refsys.UserData;
      end

      %---------------
      function sys = copySubMetaData(refsys, sys, Indu)
         % Copy meta data when inputs of sys are a subset of those of
         % refsys.

         % length(Indu) must be equal to input size of sys
         sys.OutputName_ = refsys.OutputName_;
         sys.OutputUnit_ = refsys.OutputUnit_;
         sys.OutputGroup_ = refsys.OutputGroup_;
         sys.Name_ = refsys.Name_;
         sys.Notes_ = refsys.Notes_;
         sys.UserData = refsys.UserData;

         if ~isempty(refsys.InputName_)
            sys.InputName_ = refsys.InputName_(Indu);
         end

         if ~isempty(refsys.InputUnit_)
            sys.InputUnit_ = refsys.InputUnit_(Indu);
         end

         ig = refsys.InputGroup_;
         if ~isempty(ig)
            if isstruct(ig)
               fl = fieldnames(ig);
               for ct = 1:length(fl)
                  Ind = intersect(ig.(fl{ct}),Indu);
                  if ~isempty(Ind)
                     ig.(fl{ct}) = Ind;
                  else
                     ig = rmfield(ig,fl{ct});
                  end
               end
            elseif iscell(ig)
               Remove = [];
               for ct = 1:size(ig,1)
                  Ind = intersect(ig{ct,1},Indu);
                  if ~isempty(Ind)
                     ig{ct,1} = Ind;
                  else
                     Remove = [Remove, ct]; %#ok<AGROW>
                  end
               end
               if any(Remove)
                  ig(Remove,:) = [];
               end
            end
            sys.InputGroup_ = ig;
         end
      end

      %-----------
      function sys = copyMetaDataWhenInputEqualsNoise(refsys, sys, Var)
         % Copy meta data when system's inputs have been completely
         % replaced by noise channels.
         sys.OutputName_ = refsys.OutputName_;
         sys.OutputUnit_ = refsys.OutputUnit_;
         sys.OutputGroup_ = refsys.OutputGroup_;
         sys.Name_ = refsys.Name_;
         sys.Notes_ = refsys.Notes_;
         sys.UserData = refsys.UserData;
         sys.InputName_ = idpack.utCreateNoiseInputNames(sys.OutputName_,Var,sys.IOSize_(1));
         sys.InputUnit_ = strings(0,1); % reset input unit
         sys.InputGroup_ = struct('Noise',1:sys.IOSize_(1));
      end

      %------------
      function sys = copyMetaDataWhenNoiseAddsToInputs(refsys, sys, Var)
         % Copy meta data when noise channels are appended to the end of
         % regular system inputs.
         sys = copyMetaData(refsys, sys);
         IOSize = refsys.IOSize_; Ny = IOSize(1); Nu = IOSize(2);
         NoiseInputName = idpack.utCreateNoiseInputNames(refsys.OutputName_,Var,Ny);
         sys.InputName_ = [ltipack.fullstring(refsys.InputName_,Nu) ; NoiseInputName];
         if ~isempty(sys.InputUnit_)
            sys.InputUnit_ = [sys.InputUnit_; strings(Ny,1)];
         end
         ig = sys.InputGroup_;
         if isstruct(ig)
            if Nu>0, ig.Measured = 1:Nu; end
            if (Ny+Nu)>0, ig.Noise = Nu+(1:Ny); end
         elseif iscell(ig)
            if Nu>0, ig = [ig;{1:Nu, 'Measured'}]; end
            if (Ny+Nu)>0, ig = [ig; {Nu+(1:Ny),'Noise'}]; end
         end
         sys.InputGroup_ = ig;
      end

      %------------
      function sys = resetMetaData(sys)
         % Restores default values
         sys.InputName_ = strings(0,1);  sys.OutputName_ = strings(0,1);
         sys.InputUnit_ = strings(0,1);  sys.OutputUnit_ = strings(0,1);
         sys.InputGroup_ = struct;  sys.OutputGroup_ = struct;
         sys.Name_ = '';  sys.Notes_ = strings(0,1);  sys.UserData = [];
      end

      %------------
      function sys = iocatMetaData(dim,sys,sys1,sys2)
         % Metadata management in HORZCAT/VERTCAT/APPEND
         if any(dim==1)
            sys = catOutput(sys,sys1,sys2);
         else
            sys = plusOutput(sys,sys1,sys2);
         end
         if any(dim==2)
            sys = catInput(sys,sys1,sys2);
         else
            sys = plusInput(sys,sys1,sys2);
         end
         sys.Name_ = '';  sys.Notes_ = strings(0,1);  sys.UserData = [];
      end

      %------------
      function sys = plusMetaData(sys,sys1,sys2)
         % Metadata management in PLUS
         sys = plusInput(sys,sys1,sys2);
         sys = plusOutput(sys,sys1,sys2);
         sys.Name_ = '';  sys.Notes_ = strings(0,1);  sys.UserData = [];
      end

      %------------
      function sys = mtimesMetaData(sys,sys1,sys2,ScalarFlags)
         % Metadata management in MTIMES
         if ScalarFlags(1)
            % Scalar multiplication: keep SYS2's metadata if SYS1 is scalar
            sys = copyMetaData(sys2,sys);
         elseif ScalarFlags(2)
            sys = copyMetaData(sys1,sys);
         else
            sys.InputName_ = sys2.InputName_;
            sys.InputUnit_ = sys2.InputUnit_;
            sys.InputGroup_ = sys2.InputGroup_;
            sys.OutputName_ = sys1.OutputName_;
            sys.OutputUnit_ = sys1.OutputUnit_;
            sys.OutputGroup_ = sys1.OutputGroup_;
            sys.Name_ = '';  sys.Notes_ = strings(0,1);  sys.UserData = [];
         end
      end

      %------------
      function sys = feedbackMetaData(sys,sys1,~)
         % Metadata management in FEEDBACK
         sys.InputName_ = sys1.InputName_;
         sys.InputUnit_ = sys1.InputUnit_;
         sys.InputGroup_ = sys1.InputGroup_;
         sys.OutputName_ = sys1.OutputName_;
         sys.OutputUnit_ = sys1.OutputUnit_;
         sys.OutputGroup_ = sys1.OutputGroup_;
         sys.Name_ = '';  sys.Notes_ = strings(0,1);  sys.UserData = [];
      end

      %------------
      function sys = lftMetaData(sys,sys1,sys2,indu1,indy1,indu2,indy2)
         % Metadata management in LFT
         [ny1,nu1] = iosize(sys1);
         [ny2,nu2] = iosize(sys2);
         indw1 = 1:nu1; indw1(indu1) = [];  nw1 = length(indw1);
         indz1 = 1:ny1; indz1(indy1) = [];  nz1 = length(indz1);
         indw2 = 1:nu2; indw2(indu2) = [];  nw2 = length(indw2);
         indz2 = 1:ny2; indz2(indy2) = [];  nz2 = length(indz2);

         % I/O names/units
         if isempty(sys1.InputName_) && isempty(sys2.InputName_)
            sys.InputName_ = strings(0,1);
         else
            In1 = ltipack.fullstring(sys1.InputName_,nu1);
            In2 = ltipack.fullstring(sys2.InputName_,nu2);
            sys.InputName_ = localCompress([In1(indw1,:) ; In2(indw2,:)]); % could be all empty!
         end
         if isempty(sys1.InputUnit_) && isempty(sys2.InputUnit_)
            sys.InputUnit_ = strings(0,1);
         else
            In1 = ltipack.fullstring(sys1.InputUnit_,nu1);
            In2 = ltipack.fullstring(sys2.InputUnit_,nu2);
            sys.InputUnit_ = localCompress([In1(indw1,:) ; In2(indw2,:)]);
         end
         if isempty(sys1.OutputName_) && isempty(sys2.OutputName_)
            sys.OutputName_ = strings(0,1);
         else
            Out1 = ltipack.fullstring(sys1.OutputName_,ny1);
            Out2 = ltipack.fullstring(sys2.OutputName_,ny2);
            sys.OutputName_ = localCompress([Out1(indz1,:) ; Out2(indz2,:)]);
         end
         if isempty(sys1.OutputUnit_) && isempty(sys2.OutputUnit_)
            sys.OutputUnit_ = strings(0,1);
         else
            Out1 = ltipack.fullstring(sys1.OutputUnit_,ny1);
            Out2 = ltipack.fullstring(sys2.OutputUnit_,ny2);
            sys.OutputUnit_ = localCompress([Out1(indz1,:) ; Out2(indz2,:)]);
         end

         % I/O groups
         sys.InputGroup_ = groupcat(...
            groupref(sys1.InputGroup_,indw1),...
            groupref(sys2.InputGroup_,indw2),nw1+1:nw1+nw2);
         sys.OutputGroup_ = groupcat(...
            groupref(sys1.OutputGroup_,indz1),...
            groupref(sys2.OutputGroup_,indz2),nz1+1:nz1+nz2);

         sys.Name_ = '';  sys.Notes_ = strings(0,1);  sys.UserData = [];
      end

      %------------
      function sys = parenReferenceMetaData(sys,indrow,indcol)
         % Metadata management in I/O selection operation SYS(indrow,indcol)
         sys.Notes_ = strings(0,1);
         sys.UserData = [];

         % Check for duplicated I/O names
         if isDuplicated(sys.OutputName_,indrow) || ...
               isDuplicated(sys.InputName_,indcol)
            warning(message('Control:ltiobject:DuplicatedIONames'))
         end

         % Set input names, units, and groups
         if ~isempty(sys.InputName_)
            sys.InputName_ = localCompress(sys.InputName_(indcol,1));
         end
         if ~isempty(sys.InputUnit_)
            sys.InputUnit_ = localCompress(sys.InputUnit_(indcol,1));
         end
         sys.InputGroup_ = groupref(sys.InputGroup_,indcol);

         % Set output names, units, and groups
         if ~isempty(sys.OutputName_)
            sys.OutputName_ = localCompress(sys.OutputName_(indrow,1));
         end
         if ~isempty(sys.OutputUnit_)
            sys.OutputUnit_ = localCompress(sys.OutputUnit_(indrow,1));
         end
         sys.OutputGroup_ = groupref(sys.OutputGroup_,indrow);
      end

      function sys = parenAssignMetaData(sys,indrow,indcol,iscolon,rhs,ioSize)
         % Update Input metadata
         IgnoreClash = all(iscolon([1 3:end]));
         sys.InputName_ = localStringAssign(sys.InputName_,ioSize(2),rhs.InputName_,...
            indcol,IgnoreClash,'InputName');
         sys.InputUnit_ = localStringAssign(sys.InputUnit_,ioSize(2),rhs.InputUnit_,...
            indcol,IgnoreClash,'InputUnit');
         if IgnoreClash
            % Subset of columns are fully reassigned (SYS(:,j,:,...,:) = RHS)
            sys.InputGroup_ = groupasgn(sys.InputGroup_,indcol,rhs.InputGroup_);
         else
            % Columns are only partially reassigned
            % Compare input groups. Overwrite only if original was empty
            if islogical(indcol)
               indcol = find(indcol);
            end
            Glhs = groupref(sys.InputGroup_,indcol);
            [Grhs,clash] = mrggroup(Glhs,rhs.InputGroup_);
            if clash
               warning(message('Control:ltiobject:InputGroupClash'))
            else
               if iscolon(2)
                  indcol = 1:ioSize(2);
               end
               sys.InputGroup_ = groupasgn(sys.InputGroup_,indcol,Grhs);
            end
         end

         % Update Output metadata
         IgnoreClash = all(iscolon(2:end));
         sys.OutputName_ = localStringAssign(sys.OutputName_,ioSize(1),rhs.OutputName_,...
            indrow,IgnoreClash,'OutputName');
         sys.OutputUnit_ = localStringAssign(sys.OutputUnit_,ioSize(1),rhs.OutputUnit_,...
            indrow,IgnoreClash,'OutputUnit');
         if IgnoreClash
            sys.OutputGroup_ = groupasgn(sys.OutputGroup_,indrow,rhs.OutputGroup_);
         else
            % Compare output groups. Overwrite only if original was empty
            if islogical(indrow)
               indrow = find(indrow);
            end
            Glhs = groupref(sys.OutputGroup_,indrow);
            [Grhs,clash] = mrggroup(Glhs,rhs.OutputGroup_);
            if clash
               warning(message('Control:ltiobject:OutputGroupClash'))
            else
               if iscolon(1)
                  indrow = 1:ioSize(1);
               end
               sys.OutputGroup_ = groupasgn(sys.OutputGroup_,indrow,Grhs);
            end
         end
      end

      function sys = parenDeleteMetaData(sys,indrow,indcol,iscolon)
         % Update metadata
         sizes = size(sys);
         if all(iscolon) || ~iscolon(1)
            % Output deletion
            ikeep = 1:sizes(1);   ikeep(indrow) = [];
            if isempty(ikeep)
               sys.OutputName_ = strings(0,1);   sys.OutputUnit_ = strings(0,1);
            else
               if ~isempty(sys.OutputName_)
                  sys.OutputName_(indrow,:) = [];
               end
               if ~isempty(sys.OutputUnit_)
                  sys.OutputUnit_(indrow,:) = [];
               end
            end
            sys.OutputGroup_ = groupref(sys.OutputGroup_,ikeep);
         elseif ~iscolon(2)
            % Input deletion
            ikeep = 1:sizes(2);    ikeep(indcol) = [];
            if isempty(ikeep)
               sys.InputName_ = strings(0,1);   sys.InputUnit_ = strings(0,1);
            else
               if ~isempty(sys.InputName_)
                  sys.InputName_(indcol,:) = [];
               end
               if ~isempty(sys.InputUnit_)
                  sys.InputUnit_(indcol,:) = [];
               end
            end
            sys.InputGroup_ = groupref(sys.InputGroup_,ikeep);
         end
      end

      function sys = imp2expMetaData(sys,yidx,uidx)
         % Metadata management for IMP2EXP
         InName = sys.InputName_;
         if isempty(InName)
            sys.OutputName_ = strings(0,1);
         else
            sys.InputName_ = InName(uidx,:);
            sys.OutputName_ = InName(yidx,:);
         end
         InUnit = sys.InputUnit_;
         if isempty(InUnit)
            sys.OutputUnit_ = strings(0,1);
         else
            sys.InputUnit_ = InUnit(uidx,:);
            sys.OutputUnit_ = InUnit(yidx,:);
         end
         InGroup = sys.InputGroup_;
         if isempty(InGroup)
            sys.OutputGroup_ = struct;
         else
            sys.InputGroup_ = groupref(InGroup,uidx);
            sys.OutputGroup_ = groupref(InGroup,yidx);
         end
         % Delete name, notes and userdata
         sys.Name_ = '';  sys.Notes_ = strings(0,1);  sys.UserData = [];
      end
      
      function sys = invMetaData(sys)
         % Metadata handling for INV
         if ~isempty(sys.Name_)
            sys.Name = sprintf('inv(%s)',sys.Name_);
         end
         sys.Notes_ = strings(0,1);  sys.UserData = [];
         % Swap I/O names and I/O groups
         [sys.InputName_,sys.OutputName_] = deal(sys.OutputName_,sys.InputName_);
         [sys.InputUnit_,sys.OutputUnit_] = deal(sys.OutputUnit_,sys.InputUnit_);
         [sys.InputGroup_,sys.OutputGroup_] = deal(sys.OutputGroup_,sys.InputGroup_);
      end
      
      function sys = mpowerMetaData(sys,k)
         % Metadata handling for MPOWER
         if k<0
            % Swap I/O names and  I/O groups
            [sys.InputName_,sys.OutputName_] = deal(sys.OutputName_,sys.InputName_);
            [sys.InputUnit_,sys.OutputUnit_] = deal(sys.OutputUnit_,sys.InputUnit_);
            [sys.InputGroup_,sys.OutputGroup_] = deal(sys.OutputGroup_,sys.InputGroup_);
         end
         sys.Name_ = '';  sys.Notes_ = strings(0,1);  sys.UserData = [];
      end
      
      function sys = repmatMetaData(sys,s)
         if all(s(1:2)==1)
            sys.Name_ = '';  sys.Notes_ = strings(0,1);  sys.UserData = [];
         else
            % I/O size changing
            sys = resetMetaData(sys);
         end
      end
      
      %-------------------------------------------------
      function sys = plusInput(sys,sys1,sys2)
         % Manages input channel metadata in plus-like operations
         % I/O channel names/units should match
         [sys.InputName_,clash] = ltipack.mrgname(sys1.InputName_,sys2.InputName_);
         if clash
            warning(message('Control:combination:InputNameClash'))
            sys.InputName_ = strings(0,1);
         end
         [sys.InputUnit_,clash] = ltipack.mrgname(sys1.InputUnit_,sys2.InputUnit_);
         if clash
            warning(message('Control:combination:InputUnitClash'))
            sys.InputUnit_ = strings(0,1);
         end
         % I/O groups should match
         [sys.InputGroup_,clash] = mrggroup(sys1.InputGroup_,sys2.InputGroup_);
         if clash
            warning(message('Control:combination:InputGroupClash'))
            sys.InputGroup_ = struct;
         end
      end
      
      function sys = plusOutput(sys,sys1,sys2)
         % Manages output channel metadata in plus-like operations
         [sys.OutputName_,clash] = ltipack.mrgname(sys1.OutputName_,sys2.OutputName_);
         if clash
            warning(message('Control:combination:OutputNameClash'))
            sys.OutputName_ = strings(0,1);
         end
         [sys.OutputUnit_,clash] = ltipack.mrgname(sys1.OutputUnit_,sys2.OutputUnit_);
         if clash
            warning(message('Control:combination:OutputUnitClash'))
            sys.OutputUnit_ = strings(0,1);
         end
         [sys.OutputGroup_,clash] = mrggroup(sys1.OutputGroup_,sys2.OutputGroup_);
         if clash
            warning(message('Control:combination:OutputGroupClash'))
            sys.OutputGroup_ = struct;
         end
      end
      
      %-------------------------------------------------
      function sys = catInput(sys,sys1,sys2)
         % Manages input channel metadata in concatenation operations
         nu1 = sys1.IOSize_(2);   nu2 = sys2.IOSize_(2);
         % InputName
         if isempty(sys1.InputName_) && isempty(sys2.InputName_)
            sys.InputName_ = strings(0,1);
         else
            sys.InputName_ = [ltipack.fullstring(sys1.InputName_,nu1) ; ltipack.fullstring(sys2.InputName_,nu2)];
         end
         % InputUnit
         if isempty(sys1.InputUnit_) && isempty(sys2.InputUnit_)
            sys.InputUnit_ = strings(0,1);
         else
            sys.InputUnit_ = [ltipack.fullstring(sys1.InputUnit_,nu1) ; ltipack.fullstring(sys2.InputUnit_,nu2)];
         end
         % InputGroup
         sys.InputGroup_ = groupcat(sys1.InputGroup_,sys2.InputGroup_,nu1+1:nu1+nu2);
      end
      
      function sys = catOutput(sys,sys1,sys2)
         % Manages output channel metadata in concatenation operations
         ny1 = sys1.IOSize_(1);   ny2 = sys2.IOSize_(1);
         % OutputName
         if isempty(sys1.OutputName_) && isempty(sys2.OutputName_)
            sys.OutputName_ = strings(0,1);
         else
            sys.OutputName_ = [ltipack.fullstring(sys1.OutputName_,ny1) ; ltipack.fullstring(sys2.OutputName_,ny2)];
         end
         % OutputUnit
         if isempty(sys1.OutputUnit_) && isempty(sys2.OutputUnit_)
            sys.OutputUnit_ = strings(0,1);
         else
            sys.OutputUnit_ = [ltipack.fullstring(sys1.OutputUnit_,ny1) ; ltipack.fullstring(sys2.OutputUnit_,ny2)];
         end
         % OutputGroup
         sys.OutputGroup_ = groupcat(sys1.OutputGroup_,sys2.OutputGroup_,ny1+1:ny1+ny2);
      end
      
      %------------
      function sys = augmentInput(sys,InNames,InUnits)
         % Adds set of channels to existing input channels         
         % Append new channels to output names/units
         InNames = [sys.InputName ; InNames];
         if all(strcmp(InNames,''))
            sys.InputName_ = strings(0,1);
         else
            sys.InputName_ = string(InNames);
         end
         InUnits = [sys.InputUnit ; InUnits];
         if all(strcmp(InUnits,''))
            sys.InputUnit_ = strings(0,1);
         else
            sys.InputUnit_ = string(InUnits);
         end
      end

      function sys = augmentOutput(sys,OutNames,OutUnits,GroupName)
         % Adds set of channels to existing output channels

         % Add output group for newchannels
         if nargin>3
            ny = sys.IOSize_(1);
            ny2 = length(OutNames);
            if iscell(sys.OutputGroup_)
               Group = {1:ny2 , GroupName};
            else
               Group = struct(GroupName,1:ny2);
            end
            sys.OutputGroup_ = groupcat(sys.OutputGroup_,Group,ny+1:ny+ny2);
         end
         
         % Append new channels to output names/units
         OutNames = [sys.OutputName ; OutNames];
         if all(strcmp(OutNames,''))
            sys.OutputName_ = strings(0,1);
         else
            sys.OutputName_ = string(OutNames);
         end
         OutUnits = [sys.OutputUnit ; OutUnits];
         if all(strcmp(OutUnits,''))
            sys.OutputUnit_ = strings(0,1);
         else
            sys.OutputUnit_ = string(OutUnits);
         end
      end
      
      function sys = transferInputOutput(sys,rhs,iy,iu)
         % Transfers I/O names, units, and groups from RHS to SYS starting
         % at the I/O channels IU and IY. Used by GETLFTMODEL and LFTDATA
         [ny,nu] = iosize(rhs);
         sys.InputName(iu+1:iu+nu,:) = rhs.InputName;
         sys.InputUnit(iu+1:iu+nu,:) = rhs.InputUnit;
         sys.InputGroup = groupcat(struct,rhs.InputGroup,iu+1:iu+nu);
         sys.OutputName(iy+1:iy+ny,:) = rhs.OutputName;
         sys.OutputUnit(iy+1:iy+ny,:) = rhs.OutputUnit;
         sys.OutputGroup = groupcat(struct,rhs.OutputGroup,iy+1:iy+ny);
      end
      
      function sys = reload(sys,s)
         % Restore metadata when loading object
         sys.IOSize_ = [length(s.OutputName),length(s.InputName)];
         sys.InputName = s.InputName;
         sys.OutputName = s.OutputName;
         sys.InputGroup = s.InputGroup;
         sys.OutputGroup = s.OutputGroup;
         sys.Name = s.Name;
         sys.Notes = s.Notes;
         sys.UserData = s.UserData;
      end

   end

   %% PROTECTED METHODS
   methods (Access = protected)
      
      M = removeInputBranching_(M,I,J);

      function Value = getInputName_(sys)
         % Can be overloaded to adjust default names
         Value = ltipack.fullstring(sys.InputName_,sys.IOSize_(2));
      end
      
      function Value = getOutputName_(sys)
         Value = ltipack.fullstring(sys.OutputName_,sys.IOSize_(1));
      end
      
      function sys = setInputName_(sys, Value)
         % Set input names
         % Default implementation; overridden in @idnlmodel
         sys.InputName_ = ltipack.mustBeStringVector(Value,'InputName',true);
         if sys.CrossValidation_
            sys = checkInputConsistency(sys);
         end
      end
      
      function sys = setOutputName_(sys, Value)
         % Set output names
         % Default implementation; overridden in @idnlmodel
         sys.OutputName_ = ltipack.mustBeStringVector(Value,'OutputName',true);
         if sys.CrossValidation_
            sys = checkOutputConsistency(sys);
         end
      end

      function sys = checkInputConsistency(sys)
         % Checks InputName/InputUnit consistency with input size
         nu = sys.IOSize_(2);
         if ~any(numel(sys.InputName_)==[nu 0])
            if isscalar(sys.InputName_)
               % Scalar expansion: u -> u(1),u(2),...
               sys.InputName_ = sys.InputName_ + "(" + (1:nu)' + ")";
            else
               error(message('Control:ltiobject:ltiProperties05'))
            end
         end
         if ~any(numel(sys.InputUnit_)==[nu 0])
            error(message('Control:ltiobject:ltiProperties15'))
         end
      end
      
      function sys = checkOutputConsistency(sys)
         % Checks OutputName/OutputUnit consistency with output size
         ny = sys.IOSize_(1);
         if ~any(numel(sys.OutputName_)==[ny 0])
            if isscalar(sys.OutputName_)
               % Scalar expansion: y -> y(1),y(2),...
               sys.OutputName_ = sys.OutputName_ + "(" + (1:ny)' + ")";
            else
               error(message('Control:ltiobject:ltiProperties06'))
            end
         end
         if ~any(numel(sys.OutputUnit_)==[ny 0])
            error(message('Control:ltiobject:ltiProperties16'))
         end
      end
      
      function sys = checkInputGroup(sys)
         % Checks InputGroup consistency
         try
            iGroups = getgroup(sys.InputGroup_);
         catch %#ok<CTCH>
            iGroups = [];
         end
         if ~isa(iGroups,'struct') || ~isequal(size(iGroups),[1 1])
            error(message('Control:ltiobject:ltiProperties07'))
         end
         f = fieldnames(iGroups);
         Nu = sys.IOSize_(2);
         for ct=1:length(f)
            channels = iGroups.(f{ct});
            if ~isnumeric(channels) || ~isvector(channels) || ...
                  isempty(channels) || size(channels,1)~=1 || ...
                  ~isequal(channels,round(channels))
               error(message('Control:ltiobject:ltiProperties08'))
            elseif any(channels<1) || any(channels>Nu)
               error(message('Control:ltiobject:ltiProperties09'))
            elseif length(unique(channels))<length(channels)
               error(message('Control:ltiobject:ltiProperties10'))
            end
         end
      end
      
      function sys = checkOutputGroup(sys)
         % Checks OutputGroup consistency
         try
            oGroups = getgroup(sys.OutputGroup_);
         catch %#ok<CTCH>
            oGroups = [];
         end
         if ~isa(oGroups,'struct') || ~isequal(size(oGroups),[1 1])
            error(message('Control:ltiobject:ltiProperties11'))
         end
         f = fieldnames(oGroups);
         Ny = sys.IOSize_(1);
         for ct=1:length(f)
            channels = oGroups.(f{ct});
            if ~isnumeric(channels) || ~isvector(channels) || ...
                  isempty(channels) || size(channels,1)~=1 || ...
                  ~isequal(channels,round(channels))
               error(message('Control:ltiobject:ltiProperties12'))
            elseif any(channels<1) || any(channels>Ny)
               error(message('Control:ltiobject:ltiProperties13'))
            elseif length(unique(channels))<length(channels)
               error(message('Control:ltiobject:ltiProperties14'))
            end
         end
      end
      
      function sys = fastSet(sys,varargin)
         % Version of SET without consistency checks
         ltipack.mustBeNameValuePairs(varargin)
         PublicProps = ltipack.allprops(sys);
         ClassName = class(sys);
         sys.CrossValidation_ = false;
         for ct=1:2:numel(varargin)
            % Note: ltipack.matchProperty still needed to prevent assignment 
            % to protected or private properties of ltipack.LabeledIOModel
            sys.(ltipack.matchProperty(varargin{ct},PublicProps,ClassName)) = varargin{ct+1};
         end
         sys.CrossValidation_ = true;
      end

      function indices = name2index(sys,indstr,ioflag)
         % Turn references by name into regular subscripts
         %
         %   IND = NAME2INDEX(SYS,STRCELL,IOFLAG) takes a string
         %   vector STRCELL and looks for matching I/O channel or
         %   I/O group names in the system SYS.  The search is
         %   carried out among the outputs if IOFLAG=1, and among
         %   the inputs if IOFLAG=2.
         if isnumeric(indstr) || islogical(indstr) || (ischar(indstr) && strcmp(indstr,':'))
            indices = indstr;   return
         end
         
         % Make sure input is a cell array of strings
         try
            indstr = string(indstr);
         catch 
            error(message('Control:ltiobject:subsref1',ioflag))
         end
         if ~isvector(indstr)
            error(message('Control:ltiobject:subsref6'))
         end
         
         % Set name lists for search based on IOFLAG
         if ioflag==1
            ChannelNames = ltipack.fullstring(sys.OutputName_,sys.IOSize_(1));
            Groups = getgroup(sys.OutputGroup);
         else
            ChannelNames = ltipack.fullstring(sys.InputName_,sys.IOSize_(2));
            Groups = getgroup(sys.InputGroup);
         end
         GroupNames = string(fieldnames(Groups));
         
         % Perform a string-by-string matching to respect the
         % referencing order
         indices = zeros(1,0);
         nu = length(ChannelNames);
         for ix = 1:numel(indstr)
            str = indstr(ix);
            if str==""
               error(message('Control:ltiobject:subsref7',str))
            end
            % Match against channel names and group names
            imatch = localFindMatch(str,[ChannelNames;GroupNames]);
            imatch1 = imatch(imatch<=nu);     % Channel name matches
            imatch2 = imatch(imatch>nu)-nu;   % Group name matches
            nhits1 = numel(imatch1);
            nhits2 = numel(imatch2);
            % Error checks
            if nhits1==0 && nhits2==0
               error(message('Control:ltiobject:subsref7',str))
            elseif nhits1>0 && nhits2>0
               error(message('Control:ltiobject:subsref8',str))
            elseif nhits2>0
               % Group match
               if nhits2>1
                  warning(message('Control:ltiobject:MultipleGroupMatch',str))
               end
               for ct=1:length(imatch2)
                  indices = [indices , Groups.(GroupNames{imatch2(ct)})]; %#ok<AGROW>
               end
            else
               % Channel match
               if nhits1>1
                  % Throw warning unless referring to a vector signal
                  SignalNames = regexprep(ChannelNames(imatch),'(\(\d+\))?$','');
                  if any(SignalNames~=SignalNames(1))
                     warning(message('Control:ltiobject:MultipleChannelMatch',str))
                  end
               end
               indices = [indices , imatch1(:)']; %#ok<AGROW>
            end
         end
      end
      
      function dispGroup(sys)
         % Creates display for metadata
         % I/O groups
         disp(localFormatGroup(getgroup(sys.InputGroup_), ...
             getString(message('Control:ltiobject:DispInputGroups'))))
         disp(localFormatGroup(getgroup(sys.OutputGroup_),...
             getString(message('Control:ltiobject:DispOutputGroups'))))
      end

   end

   %% HIDDEN METHODS
   methods(Hidden)

      function [InputNames,OutputNames] = mrgios(varargin)
         %MRGIOS  Compiles master I/O name list from I/Os of individual models.
         %
         %   [INPUTNAMES,OUTPUTNAMES] = MRGIOS(SYS1,SYS2,...) returns I/O names
         %   for the minimal axes grid containing all the systems SYS1, SYS2,...
         nsys = length(varargin);
         if nsys==1
            % Single model
            sys = varargin{1};
            InputNames = sys.InputName;
            OutputNames = sys.OutputName;
         else
            % Merge name
            InputNameList = cell(1,nsys);
            OutputNameList = cell(1,nsys);
            for ct=1:nsys
               sys = varargin{ct};
               InputNameList{ct} = sys.InputName;
               OutputNameList{ct} = sys.OutputName;
            end
            % Detect empty systems
            Ninputs = cellfun('length',InputNameList);
            Noutputs = cellfun('length',OutputNameList);
            % Merge names
            InputNames = LocalGetNames(InputNameList,Ninputs);
            OutputNames = LocalGetNames(OutputNameList,Noutputs);
         end

         % Always make sure the I/O sizes are nonzero
         if isempty(InputNames)
            InputNames = {''};
         end
         if isempty(OutputNames)
            OutputNames = {''};
         end
      end

      function sys = removeInputBranching(sys)
         % Removes input branching associated with duplicate input names.
         % Used by CONNECT, requires all names to be defined.
         [UniqueNames,I,J] = unique(sys.InputName_,'stable');
         if numel(I)<numel(J)
            sys = removeInputBranching_(sys,I,J);
            sys.InputName_ = UniqueNames;
            if ~isempty(sys.InputUnit_)
               sys.InputUnit_ = sys.InputUnit_(I,:);
            end
            % Note: InputGroup are somewhat arbitrary if not defined
            % consistently for all branched signals
            sys.InputGroup_ = groupref(sys.InputGroup_,I);
            sys.IOSize_ = [sys.IOSize_(1) numel(I)];
         end
      end

      function [InputName,OutputName] = getIOName(sys)
         % Used for named-based IC support in PARALLEL,...
         InputName = sys.InputName_;
         OutputName = sys.OutputName_;
      end
            
      % Note: Obsolete, here for access to GETGROUP
      function [tito,U1,U2,Y1,Y2]=istito(sys)
         % [TITO,U1,U2,Y1,Y2]=ISTITO(SYS) tests whether a system has at
         % least two InputGroup and two OutputGroup channels.
         % If not, it returns TITO=0 (false); otherwise, TITO=1 (true)
         % is returned along with TITO channel indices in row vectors:
         %        U1 = first input channel indices
         %        U2 = second input channel indices
         %        Y1 = first output channel indices
         %        Y2 = second output channel indices
         %  See also MKTITO
         ig = getgroup(sys.InputGroup_);  % struct
         og = getgroup(sys.OutputGroup_);
         Fi = fieldnames(ig);
         Fo = fieldnames(og);
         tito = min(numel(Fi),numel(Fo))>=2;
         if tito
            U1 = ig.(Fi{1});  U2 = ig.(Fi{2});
            Y1 = og.(Fo{1});  Y2 = og.(Fo{2});
         else
            U1=[]; U2=[]; Y1=[]; Y2=[];
         end
      end

   end

   methods(Static, Hidden)

      function sys = updateMetaData(sys)
         % Reload pre-R2017b metadata
         if sys.Version_<19
            sys.InputName_ = string(sys.InputName_(:));
            sys.InputUnit_ = string(sys.InputUnit_(:));
            sys.OutputName_ = string(sys.OutputName_(:));
            sys.OutputUnit_ = string(sys.OutputUnit_(:));
            if isempty(sys.Notes_)
               sys.Notes_ = strings(0,1);
            end
            if isempty(sys.Name_)
               sys.Name_ = '';
            end
         end
      end

   end

end


%--------------------- Local Functions ----------------------------

%
function g = localRemoveEmptyGroups(g)
% Removes groups with empty channel sets (simplifies group
% creation in functions like LQGTRACK)
ie = structfun(@isempty,g);
if any(ie)
   f = fieldnames(g);
   g = rmfield(g,f(ie));
end
end

%
function boo = isDuplicated(Names,Indices)
% Checks for I/O name duplications as in sys(:,[1 1])
if ~isempty(Names) && isnumeric(Indices)
   Indices = sort(Indices);
   boo = any(Names(Indices(diff(Indices)==0))~="");
else
   boo = false;
end
end

%
function imatch = localFindMatch(str,names)
% Find all NAMES matching the string STR using a cascade of matching filters

% 1) Start with partial context-insensitive matches
imatch = find(startsWith(names,str,'IgnoreCase',true));

% 2) Use case-sensitive partial matching to further narrow hits
if length(imatch)>1
   icsm = find(startsWith(names(imatch),str));
   if ~isempty(icsm)
      imatch = imatch(icsm);
   end
end

% 3) Look for exact match if there are still multiple hits.
%    Note that 'y' should be an exact match for 'y' and 'y(*)'
if length(imatch)>1
   is = regexp(names(imatch),...
      ['^' regexptranslate('escape',char(str)) '(\(\d+\))?$']);
   iexact = find(~cellfun(@isempty,is));
   if ~isempty(iexact)
      imatch = imatch(iexact);
   end
end
end

%
function s = localCompress(s)
% Do not store all-undefined names
if all(s=="")
   s = strings(0,1);
end
end

%
function Display = localFormatGroup(Group,Title)
Blank = ' ';
Names = fieldnames(Group);
ng = length(Names);
if ng>0
   Channels = cell(ng,1);
   for i=1:ng
      str = sprintf('%d,',Group.(Names{i}));
      Channels{i} = str(1:end-1);
   end
   Names = strjust(char(getString(message('Control:ltiobject:DispName1')),...
       char(Names)),'center'); %#ok<*VCAT>
   Channels = strjust(char(getString(message('Control:ltiobject:DispChannels')),...
       char(Channels)),'center');
   Display = char(Title,...
      [Blank(ones(ng+1,4)) , Names , ...
         Blank(ones(ng+1,4)) , Channels],' ');
else
   Display = '';
end
end

%
function S = localStringAssign(S,nS,R,ind,IgnoreClash,PropName)
% Handles S(ind) = R for string vectors.
% NS is the length of S after assignment
EmptyS = isempty(S);
EmptyR = isempty(R) || all(R=="");
if ~(EmptyS && EmptyR)
   % Note: Watch for possible I/O growth
   S = [S ; strings(nS-length(S),1)];
   if ~EmptyR
      if EmptyS || IgnoreClash
         % Empty S or full row/column reassignment
         S(ind,:) = R;
      else
         % Check for clashes
         [S(ind,:),clash] = ltipack.mrgname(S(ind,:),R);
         if clash
            warning(message(sprintf('Control:ltiobject:%sClash',PropName)))
         end
      end
   end
end
end

%
function names = LocalGetNames(namelist,nio)
% Compiles i/o names of individual models into master i/o name list NAMES.

% RE: The global names are picked as follows
%     1) Pick models with maximal input (resp., output) size
%     2) Among these, consider only models with all their i/o names defined
%        (model set M)
%     3) Clear all names if there are naming conflicts among models in M
% Note that 
%   * Rule 1) is needed to ensure that conflicting input names (e.g., du and dy)
%     don't result in 1x2 plot.
%   * Rule 3) is needed because there is no rigorous remapping algorithm for partial 
%     matches
%   * The resulting global I/O names should be either all defined or all empty, 
%     otherwise models in M may be mapped incorrectly (mapping algorithm ignores 
%     partial matches and mapped to upper left corner of axes grid)
%   * For this reason, name conflicts for models outside the M set are ignored 
%     and should not result in partial clearing of the global names.

% Pick models with maximal i/o size and fully specified i/o names
niomax = max(1,max(nio));  % watch for empty models
fsn = (nio == niomax);
for ct=find(fsn)
   fsn(ct) = all(cellfun('length',namelist{ct}));
end

% Aggregate names of sources with fully specified i/o names
idx = find(fsn);
if isempty(idx)
   names(1:niomax,1) = {''};
else
   names = namelist{idx(1)};
   for ct=idx(2:end)
      names(~ismember(names,namelist{ct}),:) = {''};
   end
end
end