% sDomainFit performs a fitting loop to identify model order and
% parameters.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% DESCRIPTION: sDomainFit fit a partial fraction model to frequency
%     response data using  the function utils.math.vcfit.
%
%     The function performs a fitting loop to automatically identify model
%     order and parameters in s-domain. Output is a s-domain model expanded
%     in partial fractions:
%
%              r1              rN
%     f(s) = ------- + ... + ------- + d
%            s - p1          s - pN
%
%     The function attempt to perform first the identification of a model
%     with d = 0, then if the operation do not succeed, it try the
%     identification with d different from zero.
%     %     Identification loop stop when the stop condition is reached.
%     Stop criterion is based on three different approachs:
%
%     1) Mean Squared Error and variation
%     Check if the normalized mean squared error is lower than the value specified in
%     FITTOL and if the relative variation of the mean squared error is lower
%     than the value specified in MSEVARTOL.
%     E.g. FITTOL = 1e-3, MSEVARTOL = 1e-2 search for a fit with
%     normalized magnitude error lower than 1e-3 and and MSE relative
%     variation lower than 1e-2.
%
%     1) Log residuals difference and root mean squared error
%     Log Residuals difference
%     Check if the minimum of the logarithmic difference between data and
%     residuals is larger than a specified value. ie. if the conditioning
%     value is 2, the function ensures that the difference between data and
%     residuals is at lest 2 order of magnitude lower than data itsleves.
%     Root Mean Squared Error
%     Check that the variation of the root mean squared error is lower than
%     10^(-1*value).
%
%     2) Residuals spectral flatness and root mean squared error
%     Residuals Spectral Flatness
%     In case of a fit on noisy data, the residuals from a good fit are
%     expected to be as much as possible similar to a white noise. This
%     property can be used to test the accuracy of a fit procedure. In
%     particular it can be tested that the spectral flatness coefficient of
%     the residuals is larger than a certain qiantity sf such that 0<sf<1.
%     Root Mean Squared Error
%     Check that the variation of the root mean squared error is lower than
%     10^(-1*value).
%
%     Both in the first and second approach the fitting loop stops when the
%     two stopping conditions are satisfied.
%     The output are AOs containing the frequency response of the fitted
%     model, while the Model parameters are output as a parfrac model
%     in the output AOs procinfo filed.
%
%     The function can also perform a single loop without taking care of
%     the stop conditions. This happens when 'AutoSearch' parameter is
%     setted to 'off'.
%
%     If you provide more than one AO as input, they will be fitted
%     together with a common set of poles.
%
% CALL:         mod = sDomainFit(a, pl)
%
% INPUTS:      a  - input AOs to fit to. If you provide more than one AO as
%                   input, they will be fitted together with a common set
%                   of poles. Only frequency domain (fsdata) data can be
%                   fitted. Each non fsdata object will be ignored. Input
%                   objects must have the same number of elements.
%              pl - parameter list (see below)
%
% OUTPUTS:
%               mod - matrix of one parfrac object for each input AO.
%                     Usseful fit information are stored in the procinfoi
%                     field:
%                     FIT_RESP  - model frequency response.
%                     FIT_RESIDUALS - analysis object containing the fit
%                     residuals.
%                     FIT_MSE - analysis object containing the mean squared
%                     error progression during the fitting loop.
%
%
% Note: all the input objects are assumed to caontain the same X
% (frequencies) values
%
%
% EXAMPLES:
%
% 1) Fit to a frequency-series using Mean Squared Error and variation stop
% criterion
%
%   % Create a frequency-series AO
%   pl_data = plist('fsfcn', '0.01./(0.0001+f)', 'f1', 1e-5, 'f2', 5, 'nf', 1000);
%   a = ao(pl_data);
%
%   % Fitting parameter list
%   pl_fit = plist('AutoSearch','on',...
%   'StartPoles',[],...
%   'StartPolesOpt','clog',...
%   'maxiter',5,...
%   'minorder',2,...
%   'maxorder',20,...
%   'weights',[],...
%   'CONDTYPE','MSE',...
%   'FITTOL',1e-3,...
%   'MSEVARTOL',1e-2,...
%   'Plot','off',...
%   'ForceStability','off',...
%   'direct term','off',...
%   'CheckProgress','off');
%
%   % Do fit
%   b = sDomainFit(a, pl_fit);
%
% 2) Fit to a frequency-series using Log residuals difference and mean
% squared error variation stop criterion
%
%   % Create a frequency-series AO
%   pl_data = plist('fsfcn', '0.01./(0.0001+f)', 'f1', 1e-5, 'f2', 5, 'nf', 1000);
%   a = ao(pl_data);
%
%   % Fitting parameter list
%   pl_fit = plist('FS',[],...
%   'AutoSearch','on',...
%   'StartPoles',[],...
%   'StartPolesOpt','clog',...
%   'maxiter',5,...
%   'minorder',2,...
%   'maxorder',20,...
%   'weights',[],...
%   'weightparam','abs',...
%   'CONDTYPE','RLD',...
%   'FITTOL',1e-3,...
%   'MSEVARTOL',1e-2,...
%   'Plot','off',...
%   'ForceStability','off',...
%   'CheckProgress','off');
%
%   % Do fit
%   b = sDomainFit(a, pl_fit);
%
% 3) Fit to a frequency-series using Residuals spectral flatness and mean
% squared error variation stop criterion
%
%   % Create a frequency-series AO
%   pl_data = plist('fsfcn', '0.01./(0.0001+f)', 'f1', 1e-5, 'f2', 5, 'nf', 1000);
%   a = ao(pl_data);
%
%   % Fitting parameter list
%   pl_fit = plist('FS',[],...
%   'AutoSearch','on',...
%   'StartPoles',[],...
%   'StartPolesOpt','clog',...
%   'maxiter',5,...
%   'minorder',2,...
%   'maxorder',20,...
%   'weights',[],...
%   'weightparam','abs',...
%   'CONDTYPE','RSF',...
%   'FITTOL',0.5,...
%   'MSEVARTOL',1e-2,...
%   'Plot','off',...
%   'ForceStability','off',...
%   'CheckProgress','off');
%
%   % Do fit
%   b = sDomainFit(a, pl_fit);
%
%
% <a href="matlab:utils.helper.displayMethodInfo('ao', 'sDomainFit')">Parameters Description</a>
%
% VERSION:     $Id: sDomainFit.m,v 1.32 2011/08/15 09:46:44 hewitson Exp $
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

function varargout = sDomainFit(varargin)
  
  % Check if this is a call for parameters
  if utils.helper.isinfocall(varargin{:})
    varargout{1} = getInfo(varargin{3});
    return
  end
  
  import utils.const.*
  utils.helper.msg(msg.PROC3, 'running %s/%s', mfilename('class'), mfilename);
  
  % Collect input variable names
  in_names = cell(size(varargin));
  for ii = 1:nargin,in_names{ii} = inputname(ii);end
  
  % Collect all AOs and plists
  [as, ao_invars] = utils.helper.collect_objects(varargin(:), 'ao', in_names);
  pl              = utils.helper.collect_objects(varargin(:), 'plist', in_names);
  
  if nargout == 0
    error('### sDomainFit cannot be used as a modifier. Please give an output variable.');
  end
  
  %%% Decide on a deep copy or a modify
  bs = copy(as, nargout);
  inhists = [as.hist];
  
  % combine plists
  pl = parse(pl, getDefaultPlist());
  
  %%%%% Extract necessary parameters %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  
  extpoles = find(pl, 'StartPoles'); % Check if external poles are providied
  spolesopt = 0;
  if isempty(extpoles) % if no external poles set them internally
    splopt = find(pl, 'StartPolesOpt');
    switch lower(splopt)
      case 'real'
        spolesopt = 1;
      case 'clog'
        spolesopt = 2;
      case 'clin'
        spolesopt = 3;
    end
  end
  
  maxiter = find(pl, 'maxiter'); % set the maximum number of iterations
  minorder = find(pl, 'minorder'); % set the minimum function order
  maxoredr = find(pl, 'maxorder');% set the maximum function order
  
  extweights = find(pl, 'weights'); % check if external weights are provided
  weightparam = 0;
  if isempty(extweights) % set internally the weights on the basis of the input options
    wtparam = find(pl, 'weightparam');
    switch lower(wtparam)
      case 'ones'
        weightparam = 1;
      case 'abs'
        weightparam = 2;
      case 'sqrt'
        weightparam = 3;
    end
  end
  
  % decide to plot or not
  plt = find(pl, 'plot');
  switch lower(plt)
    case 'on'
      showplot = 1;
    case 'off'
      showplot = 0;
  end
  
  % Make a decision between Fit conditioning type
  condtype = find(pl, 'CONDTYPE');
  condtype = upper(condtype);
  switch condtype
    case 'MSE'
      ctp = 'chivar'; % use normalized mean squared error value and relative variation
      lrscond = find(pl, 'FITTOL');
      % give an error for strange values of lrscond
      if lrscond<0
        error('!!! Negative values for FITTOL are not allowed !!!')
      end
      % handling data
      lrscond = -1*log10(lrscond);
      % give a warning for strange values of lrscond
      if lrscond<0
        warning('You are searching for a MSE lower than %s', num2str(10^(-1*lrscond)))
      end
    case 'RLD'
      ctp = 'lrsmse'; % use residuals log difference and MSE relative variation
      lrscond = find(pl, 'FITTOL');
      % give a warning for strange values of lrscond
      if lrscond<0
        error('!!! Negative values for FITTOL are not allowed !!!')
      end
      if lrscond<1
        warning('You are searching for a frequency by frequency residuals log difference of %s', num2str(lrscond))
      end
    case 'RSF'
      ctp = 'rftmse'; % use residuals spectral flatness and MSE relative variation
      lrscond = find(pl, 'FITTOL');
      % give a warning for strange values of lrscond
      if lrscond<0 || lrscond>1
        error('!!! Values <0 or >1 for FITTOL are not allowed when CONDTYPE is RSF !!!')
      end
  end
  
  % Tolerance for the MSE relative variation
  msevar = find(pl, 'MSEVARTOL');
  % handling data
  msevar = -1*log10(msevar);
  % give a warning for strange values of msevar
  if msevar<0
    warning('You are searching for MSE relative variation lower than %s', num2str(10^(-1*msevar)))
  end
  
  % decide to stabilize or not the model
  stab = find(pl, 'ForceStability');
  switch lower(stab)
    case 'on'
      stabfit = 1;
    case 'off'
      stabfit = 0;
  end
  
  % decide to fit with or whitout direct term
  dtm = find(pl, 'direct term');
  switch lower(dtm)
    case 'on'
      dterm = 1;
    case 'off'
      dterm = 0;
  end
  
  % decide to disp or not the fitting progress in matlab command window
  prg = find(pl, 'CheckProgress');
  switch lower(prg)
    case 'on'
      spy = 1;
    case 'off'
      spy = 0;
  end
  
  % decide to perform or not a full automatic model search
  autos = find(pl, 'AutoSearch');
  switch lower(autos)
    case 'on'
      fullauto = 1;
    case 'off'
      fullauto = 0;
  end
  
  % extract delay
  delay = find(pl, 'delay');
  
  %%%%% End Extract necessary parameters %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  
  
  
  %%%%% Fitting %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  
  % Fit parameters
  params = struct('spolesopt',spolesopt,...
    'extpoles', extpoles,...
    'Nmaxiter',maxiter,...
    'minorder',minorder,...
    'maxorder',maxoredr,...
    'weightparam',weightparam,...
    'extweights', extweights,...
    'plot',showplot,...
    'ctp',ctp,...
    'lrscond',lrscond,...
    'msevar',msevar,...
    'stabfit',stabfit,...
    'dterm',dterm,...
    'spy',spy,...
    'fullauto',fullauto);
  
  %%% extracting elements from AOs
  
  % Finding the index of the first fsdata
  for gg = 1:numel(bs)
    if isa(bs(gg).data, 'fsdata')
      prm = gg;
      break
    end
  end
  
  y = zeros(length(bs(prm).data.getY),numel(bs)); % initialize input vector
  k = numel(bs(prm).data.getY); % setting a comparison constant
  idx = true(numel(bs),1); % initialize the control index
  for jj=1:numel(bs)
    % checking that AOs are fsdata and skipping non fsdata objects
    if ~isa(bs(jj).data, 'fsdata')
      % skipping data if non fsdata
      warning('!!! %s expects ao/fsdata objects. Skipping AO %s', mfilename, ao_invars{jj});
      idx(jj) = false; % set the corresponding value of the control index to false
    else
      % preparing data for fit
      yt = bs(jj).data.getY;
      if numel(yt)~=k
        error('Input AOs must have the same number of elements')
      end
      if size(yt,2)>1 % wish to work with columns
        y(:,jj) = yt.';
      else
        y(:,jj) = yt;
      end
    end
  end
  %%% extracting frequencies
  % Note: all the objects are assumed to caontain the same X (frequencies) values
  f = bs(prm).data.getX;
  
  % reshaping y to contain only Y from fsdata, subtract delay if given by
  % user
  if ~isempty(delay)
    y = y(:,idx)./exp(-2*pi*1i*f*delay);
  else
    y = y(:,idx);
  end
  
  % Fitting loop
  [res,poles,dterm,mresp,rdl,mse] = utils.math.autocfit(y,f,params);
  
  %%%%% Building output AOs with model responses, model parameters are %%%%
  
  for kk = 1:numel(bs)
    if idx(kk) % set the corresponding Y values of fitted data
      
      % if delay is input we return a pzmodel with the corresponding delay
      if isempty(delay)
        mdl(kk) = parfrac(plist('res', res(:,kk),'poles', poles, 'dir',...
          dterm(:,kk), 'name', sprintf('fit(%s)', ao_invars{kk})));
      else
        mdl_aux = parfrac(plist('res', res(:,kk),'poles', poles, 'dir',...
          dterm(:,kk), 'name', sprintf('fit(%s)', ao_invars{kk})));
        mdl(kk) = pzmodel(mdl_aux);
        mdl(kk).setDelay(delay);
      end
      
      % Output also response, residuals and mse progression in the procinfo
      
      rsp = mresp(:,kk);
      bs(kk).data.setY(rsp);
      
      % Set output AO name
      bs(kk).name = sprintf('fit(%s)', ao_invars{kk});
      
      res_ao = copy(bs(kk),1);
      trdl = rdl(:,kk);
      res_ao.data.setY(trdl);
      
      % Set output AO name
      res_ao.name = sprintf('fit_residuals(%s)', ao_invars{kk});
      
      d = cdata();
      tmse = mse(:,kk);
      d.setY(tmse);
      mse_ao = ao(d);
      
      % Set output AO name
      mse_ao.name = sprintf('fit_mse(%s)', ao_invars{kk});
      
      procpl = plist('fit_resp',bs(kk),...
        'fit_residuals',res_ao,...
        'fit_mse',mse_ao);
      
      mdl(kk).setProcinfo(procpl);
      
    else
      mdl(kk) = parfrac();
    end
    
  end
  
  % set output as matrix if multiple inputs
  if numel(mdl) ~= 1
    mmdl = matrix(mdl);
  else
    mmdl = mdl;
  end
  
  mmdl.setName(sprintf('fit(%s)', ao_invars{:}));
  
  mmdl.addHistory(getInfo('None'), pl, [ao_invars(:)], [inhists(:)]);
  
  % ----- Set outputs -----
  if nargout == 1
    varargout{1} = mmdl;
  else
    % multiple output is not supported
    error('### Multiple output is not supported ###')
  end
  
end

%--------------------------------------------------------------------------
% Get Info Object
%--------------------------------------------------------------------------
function ii = getInfo(varargin)
  if nargin == 1 && strcmpi(varargin{1}, 'None')
    sets = {};
    pl   = [];
  else
    sets = {'Default'};
    pl   = getDefaultPlist;
  end
  % Build info object
  ii = minfo(mfilename, 'ao', 'ltpda', utils.const.categories.sigproc, '$Id: sDomainFit.m,v 1.32 2011/08/15 09:46:44 hewitson Exp $', sets, pl);
  ii.setModifier(false);
end

%--------------------------------------------------------------------------
% Get Default Plist
%--------------------------------------------------------------------------
function plout = getDefaultPlist()
  persistent pl;  
  if exist('pl', 'var')==0 || isempty(pl)
    pl = buildplist();
  end
  plout = pl;  
end

function pl = buildplist()
  
  pl = plist();
  
  % AutoSearch
  p = param({'AutoSearch', ['''on'': Parform a full automatic search for the<br>'...
    'transfer function order. The fitting<br>'...
    'procedure will stop when stop conditions<br>'...
    'defined are satisfied.<br>'...
    '''off'': Perform a fitting loop as long as the<br>'...
    'number of iteration reach ''maxiter''. The order<br>'...
    'of the fitting function will be that<br>'...
    'specified in ''minorder''.']}, ...
    {1, {'on', 'off'}, paramValue.SINGLE});
  pl.append(p);
  
  % StartPoles
  p = param({'StartPoles', ['A vector of starting poles. Providing a fixed<br>'...
    'set of starting poles fixes the function<br>'...
    'order. If it is left empty starting poles are<br>'...
    'internally assigned.']}, paramValue.EMPTY_DOUBLE);
  pl.append(p);
  
  % StartPolesOpt
  p = param({'StartPolesOpt', ['Define the characteristics of internally<br>'...
    'assigned starting poles. Admitted values<br>'...
    'are:<ul>'...
    '<li>''real'' linear-spaced real poles</li>'...
    '<li>''clog'' log-spaced complex poles</li>'...
    '<li>''clin'' linear-spaced complex poles<li></ul>']}, ...
    {2, {'real', 'clog', 'clin'}, paramValue.SINGLE});
  pl.append(p);
  
  % MaxIter
  p = param({'MaxIter', 'Maximum number of iterations in fit routine.'}, paramValue.DOUBLE_VALUE(50));
  pl.append(p);
  
  % MinOrder
  p = param({'MinOrder', 'Minimum order to fit with.'}, paramValue.DOUBLE_VALUE(2));
  pl.append(p);
  
  % MaxOrder
  p = param({'MaxOrder', 'Maximum order to fit with.'}, paramValue.DOUBLE_VALUE(20));
  pl.append(p);
  
  % Weights
  p = param({'Weights', ['A vector with the desired weights. If a single<br>'...
    'Ao is input weights must be a Nx1 vector where<br>'...
    'N is the number of elements in the input Ao. If<br>'...
    'M Aos are passed as input, then weights must<br>'...
    'be a NxM matrix. If it is leaved empty weights<br>'...
    'are internally assigned basing on the input<br>'...
    'parameters']}, paramValue.EMPTY_DOUBLE);
  pl.append(p);
  
  % Weightparam
  p = param({'weightparam', ['Specify the characteristics of the internally<br>'...
    'assigned weights. Admitted values are:<ul>'...
    '<li>''ones'' assigns weights equal to 1 to all data.<li>'...
    '<li>''abs'' weights data with <tt>1./abs(y)</tt></li>'...
    '<li>''sqrt'' weights data with <tt>1./sqrt(abs(y))</tt></li>']}, ...
    {2, {'ones', 'abs', 'sqrt'}, paramValue.SINGLE});
  pl.append(p);
  
  % CONDTYPE
  p = param({'CONDTYPE', ['Fit conditioning type. Admitted values are:<ul>'...
    '<li>''MSE'' Mean Squared Error and variation</li>'...
    '<li>''RLD'' Log residuals difference and mean squared error variation<li>'...
    '<li>''RSF'' Residuals spectral flatness and mean squared error variation<li></ul>']}, ...
    {1, {'MSE', 'RLD', 'RSF'}, paramValue.SINGLE});
  pl.append(p);
  
  % FITTOL
  p = param({'FITTOL', 'Fit tolerance.'}, paramValue.DOUBLE_VALUE(1e-3));
  pl.append(p);
  
  % MSEVARTOL
  p = param({'MSEVARTOL', ['Mean Squared Error Variation - Check if the<br>'...
    'realtive variation of the mean squared error is<br>'...
    'smaller than the value specified. This<br>'...
    'option is useful for finding the minimum of Chi-squared.']}, ...
    paramValue.DOUBLE_VALUE(1e-2));
  pl.append(p);
  
  % Plot
  p = param({'Plot', 'Plot results of each fitting step.'}, {2, {'on', 'off'}, paramValue.SINGLE});
  p.val.setValIndex(2);
  pl.append(p);
  
  % ForceStability
  p = param({'ForceStability', 'Force poles to be stable'}, ...
    {2, {'on', 'off'}, paramValue.SINGLE});
  pl.append(p);
  
  % direct term
  p = param({'direct term', 'Fit with direct term.'}, {2, {'on', 'off'}, paramValue.SINGLE});
  pl.append(p);
  
  % CheckProgress
  p = param({'CheckProgress', 'Display the status of the fit iteration.'}, ...
    {2, {'on', 'off'}, paramValue.SINGLE});
  pl.append(p);
  
  % Delay
  p = param({'delay', 'Innput a delay that will be subtracted from the fit.<br>'...
    'The output is a pzmodel which includes the inputted delay.'},paramValue.EMPTY_DOUBLE);
  pl.append(p);
end
% END


% PARAMETERS:
%             'AutoSearch'  - 'on': Parform a full automatic search for the
%                             transfer function order. The fitting
%                             procedure will stop when stop conditions
%                             defined are satisfied. [Default]
%                             'off': Perform a fitting loop as long as the
%                             number of iteration reach 'maxiter'. The order
%                             of the fitting function will be that
%                             specified in 'minorder'.
%             'StartPoles'  - A vector of starting poles. Providing a fixed
%                             set of starting poles fixes the function
%                             order. If it is left empty starting poles are
%                             internally assigned. [Default []]
%             'StartPolesOpt' - Define the characteristics of internally
%                               assigned starting poles. Admitted values
%                               are:
%                               'real' linspaced real poles
%                               'clog' logspaced complex poles [Default]
%                               'clin' linspaced complex poles
%             'maxiter'   - Maximum number of allowed iteration. [Deafult
%                           50].
%             'minorder'  - Minimum model function order. [Default 2]
%             'maxorder'  - Maximum model function order. [Default 20]
%             'weights'   - A vector with the desired weights. If a single
%                           Ao is input weights must be a Nx1 vector where
%                           N is the number of elements in the input Ao. If
%                           M Aos are passed as input, then weights must
%                           be a NxM matrix. If it is leaved empty weights
%                           are internally assigned basing on the input
%                           parameters. [Default []]
%             'weightparam' - Specify the characteristics of the internally
%                             assigned weights. Admitted values are:
%                             'ones' assigns weights equal to 1 to all data.
%                             'abs' weights data with 1./abs(y) [Default]
%                             'sqrt' weights data with 1./sqrt(abs(y))
%             'CONDTYPE'  - Fit conditioning type. Admitted values are:
%                             - 'MSE' Mean Squared Error and variation
%                             [Default]
%                             - 'RLD' Log residuals difference and mean
%                             squared error variation
%                             - 'RSF' Residuals spectral flatness and mean
%                             squared error variation
%               'FITTOL'  - Fit tolerance [Default, 1e-3]
%           'MSEVARTOL'   - This allow to check if the relative variation
%                           of mean squared error is lower than the value
%                           sepcified. [Default 1e-2]
%             'Plot'        - Plot fit result: 'on' or 'off' [default]
%             'ForceStability'  - Force poles to be stable, values are
%                                 'on' or 'off'. [Default 'off']
%             'direct term' - Fit with direct term if 'on', without if
%                             'off'. [Default 'off']
%             'CheckProgress' - Disply the status of the fit iteration.
%                               Values are 'on and 'off'. [Default 'off']
%
%

