No BSD License  

Highlights from
GUI for Generalized Nonlinear Non-analytic Chi-Square Fitting

image thumbnail
from GUI for Generalized Nonlinear Non-analytic Chi-Square Fitting by Nathaniel Brahms
Provides a Curve Fitting Toolbox-like interface for chi-square fitting

[params,dParams,gof,stddev]...
function [params,dParams,gof,stddev]...
    = fitChiSquare(xData,yData,modelFun,guess,dxData,dyData,options)
%--------------------------------------------------------------------------
%   [params,dParams,gof,stddev] = 
%       fitChiSquare(xData,yData,modelFun,guess,dxData,dyData,options)
%
%   fitChiSquare is a generalized chi-square fitting routine for any
%   model function when data measurement errors are known; it returns the
%   model parameters and their uncertainties at the delta chi-squared = 1
%   boundary (68% confidence interval).
%
%   This function uses the optimization toolbox function lsqnonlin to first
%   perform a nonlinear least squares fit of the data to the model.  It
%   then calculates the standard deviation of each point to the fit value,
%   then calculates the chi squared.  Finally, the function fzero is
%   used on each parameter to find the value at which delta chi-squared is
%   equal to 1 while minimizing chi^2 with respect to the other parameters.
%   In the case that the parameter uncertainties are normally distributed,
%   the delta chi^2 = 1 method gives the 68% confidence limit for the
%   parameters.  Monte Carlo or investigations of many data sets should be
%   used to confirm the parameter uncertainties are normally distributed.
%   This method gives upper and lower bounds for each parameter.[1,2]
%
%   If one encounters the following error message:
%
%       Unexpected termination flag 0 in non-estimating variable
%       minimization during uncertainty estimation
%
%   this is because the non-varying parameter minimization routine has
%   encountered its iteration or evaluation limit.  Raise
%   options.UncOptions.MaxFunEvals or options.UncOptions.MaxIter.
%
%   Note:  If the user can not use lsqnonlin (i.e., the optimization
%   toolbox is not installed), the program will use the built-in function
%   fminsearch instead.  This may reduce the robustness of the calculation.
%
%   Author:     N. Brahms
%   Contact:    Contact via Matlab File Exchange website
%   Version:    2.1
%   Updated:    1/25/06
%
%   Inputs:
%       xData   - a m x n matrix of independent variable data, where
%                   columns represent independent variables, and rows
%                   represent successive observations thereof
%       yData   - a length m vector of dependent variable data
%       dxData  - a m x n matrix of uncertainties corresponding to each
%                   measurement in xData
%       dxData  - a length m vector of uncertainties corresponding to each
%                   measurement in yData
%       modelFun- a pointer to a vectorized modeling function:
%
%           [result] = modelFun(params,xData)
%
%           which returns a length m vector of f(x) using the parameters
%           passed
%
%       guess   - a length k vector of initial guesses for the parameters
%       options - an options object to pass to the fitting routine;
%                 this is the same as described in optimset, but with the
%                 following additional fields (values in braces are defaults):
%
%           Display = 'iter' | {'on'} | 'off'
%               passed to the fitting routine.  If set to 'on' or 'iter',
%               also causes fitChiSquare to display the fitting results in
%               the command window.
%           Plot = {0} | 1
%               plots the fit result.  If PlotResiduals is also on, the fit
%               result is plotted in subplot [1 2] and the residuals are
%               plotted in subplot 3.
%           PlotVariable = {1} | ... | k
%               which independent variable to plot against.
%           PlotResiduals = {0} | 1
%               plots the weighted fit residuals
%           ErrorsUnknown = {0} | 1
%               if 1, assumes the model is correct and uses the model to
%               calculate the data standard deviation.  Assumes deviation
%               is uniform.
%               If using ErrorsUnknown, the delta chi2 = 1 bounds for the
%               parameters can only be interpreted as the "68%
%               confidence interval given the model is correct" (and then
%               only if the parameters are gaussian-distributed).
%               if 0, uses measurement errors to calculate the data
%               standard deviation, and fits parameter uncertainties
%           FitUncertainty = 0 | {1} | k-vector
%               if 1 (default), fits uncertainties
%               of 0, does not fit uncertainties (returns empty dparams)
%               if length k boolean vector, fits uncertainties only to 'on'
%                 indices.  Example for a three parameter fit:
%                   FitUncertianty = [0 1 1];
%           LowerBound     = vector | {[-Inf ... -Inf]}
%               a length k row vector of parameter lower bounds
%           UpperBound     = vector | {[Inf ... Inf]}
%               a length k row vector of parameter upper bounds
%           DisplayUncVal  = {0} | 1
%               shows additional information when fitting the uncertainties
%           UncOptions     = optimset options |
%                            {optimset('TolFun',1e-2,'Display','off')}
%               optimset options to pass to uncertainty solver (fzero)
%
%   Outputs:
%       params  - a length k vector of fit model parameters
%       dParams - a length k cell array of structures with the following
%                 fields:
%           d   - equals (dl+du)/2
%           dl  - absolute lower deviation at dChi^2=1
%           du  - absolute upper deviation at dChi^2=1
%           lVal- a length k vector of the value of each parameter at the
%                 dChi^2=1 projection point for the lower deviation
%           uVal- a length k vector of the value of each parameter at the
%                 dChi^2=1 projection point for the upper deviation
%       gof     - a goodness-of-fit struct with the following fields:
%           chi2- the chi-square of the fit
%           dof - degrees-of-freedom of the fit
%           eta2- the eta-square of the fit (a.k.a. correlation index) -
%                 this quantity is analogous to the r-square (correlation
%                 coefficient) of a linear least-squares fitting [3]
%       stddev  - the expected deviation from the fit for each observation
%
%   References:
%   1. W.H. Press, B.P. Flannery, S.A. Teukolsky, W.T. Vetterling.
%      Numerical Recipes; The Art of Scientific Computing. (Cambridge
%      University Press: Cambridge). 1986.
%   2. P.R. Bevington, D.K. Robinson. Data Reduction and Error Analysis
%      for the Physical Sciences. (McGraw-Hill: New York). 1992.
%   3. http://mathworld.wolfram.com
%
%--------------------------------------------------------------------------

%--------------------------------------------------------------------------
%
%   History
%       1.0     - Initial version
%       1.1     - Changed syntax and added the doError switch
%       1.1.1   - Added more detailed reporting
%       1.2     - Changed parameter uncertainty minimization function from
%                 fminbnd to fminsearch with coded constraints
%       1.3     - Changed non-estimating parameter minimization to occur
%                 inside fminsearch in uncertainty minimization.  Also,
%                 fixed a bug causing improper weighting of initial chi2
%                 minimization.  Removed doError switch.
%       1.3.1   - Minor code rearrangement
%       1.3.2   - Can run without the optimization toolbox
%       1.3.3   - Added UncMaxFunEvals & UncMaxIter options
%       1.3.4   - Fixed a bug whereby the uncertainty estimator would
%                 return bogus values instead of an error if the included
%                 non-varying parameter minimization routine halted
%                 unexpectedly.
%       2.0     - File name change to fitChiSquare.  Added example use
%                 file and included between function in release.  Added
%                 stddev output.
%       2.1     - Added bounds, uncertainty options.  Updated
%                 documentation.
%       2.2     - Added ErrorsUnknown option, cleaned up documentation.
%       2.3     - Added plotting options, changed to allow FitUncertainty
%                 with ErrorsUnknown. Changed dParams to normal array.
%       2.4     - Added gof struct - note syntax change
%       2.4.1   - Fixed bug where struct was misassigned if
%                 FitUncertainty(1) was equal to 0
%
%   Known problems / suggested features
%       - Add flags indicating that the fit has stopped at bounds
%
%--------------------------------------------------------------------------

% Uncertainty loop constants and defaults
LARGE_NUM = realmax;    % The number used in the minimization routine when
                        % the minimizing variable is out of bounds
UNC_OPT  = optimset('TolFun',1e-2,'Display','off');

initializeOptions;
opEPar = initializeUncOptions(options);

% Check degrees of freedom
if (length(yData) <= length(guess))
    error('Nonpositive degrees of freedom');
end

% Condition input
[xData, dxData, yData, dyData] = conditionInput(xData, dxData,...
    yData, dyData, ~options.ErrorsUnknown);

% Force bound vectors to be row vectors
options.LowerBound = conditionBound(options.LowerBound);
options.UpperBound = conditionBound(options.UpperBound);

% Check guess is in bounds
for ia=1:length(guess)
    if any(options.LowerBound>options.UpperBound)
        error('Lower bound exceeds upper bound')
    end
    if ~between(guess(ia),[options.LowerBound(ia) options.UpperBound(ia)],1)
        error(sprintf('Guess %d is not in bound',ia));
    end
end

% Nested function variable initialization
pVarIndex = 0;
pVar      = 0;
pFix      = 0;
pEVarVal  = zeros(length(guess)-1,1);
pEVarInd  = zeros(length(guess)-1,1);
pVarBnd   = [-Inf, Inf];
pVarMult  = 1;

if ~strcmp(options.Display,'off')
    disp('Fitting function parameters...');
end

% Calculate the degrees of freedom of the problem
dof = length(yData)-length(guess);

% Calculate the fit with uniform uncertainty
stddev = 1;
% Average deviation from the guess
stddev = sqrt(sum(model(guess).^2)/dof);

if ~options.ErrorsUnknown
    startOptions = options; startOptions.TolFun = 1e-2; startOptions.TolX = 1e-8;
    params = trylsq(@model,guess,[],[],startOptions);
    % Calculate the standard deviation for each datum
    stddev = calcStdDev;
    % Calculate the least-squares fit
    params = trylsq(@model,params,[],[],options);
else
    % Calculate the least-squares fit
    params = trylsq(@model,guess,[],[],options);
    stddev = 1;
    stddev = sqrt(sum(model(params).^2)/dof);
end

% Calculate the chi-square function
chiSquare = sum( model(params).^2 );

% Fill goodness-of-fit struct
gof.chi2 = chiSquare;
gof.dof  = dof;
gof.eta2 = (std( (yData-model(params).*stddev) ) / std( yData) )^2;

% Fits parameter uncertainties
% Be careful not to use ia in any nested functions!
for ia=1:length(params)
    if options.FitUncertainty(ia)
        dParams(ia)=delta(ia);
    else
        % As of R14SP3, the fields below MUST be in the same order as they
        % are defined in the delta function
        dParams(ia) = initializeDParams;
    end
end

% If a verbose mode is on, display the answer and reduced chi-squared
if ~strcmp(options.Display,'off')
    for id=1:length(params)
        disp(sprintf('Parameter %d = %f + %f - %f',...
            id,params(id),dParams(id).du,dParams(id).dl));
    end
    disp(sprintf('Reduced chi-square = %f',chiSquare/dof));
end

% Plot the fit
if options.Plot
    % Subplot if also plotting residuals
    if options.PlotResiduals
        subplot(3,1,[1 2]);
    end
    holdstate = ishold;
    errorbar(xData(:,options.PlotVariable),yData,stddev,'.');
    plotHandle = gca;
    hold on
    plot(xData(:,options.PlotVariable),...
        yData - model(params).*stddev,'-r');
    switch holdstate
        case 0
            hold off
    end
    xlabel('x1');
    ylabel('y');
    title('Fit plot');
    lstring = sprintf('Fit: ~\\chi^2 = %0.2g\n',chiSquare/dof);
    for ia = 1:length(params);
        lstring = [lstring ...
            sprintf('Par. %d = %0.2g + %0.1g - %0.1g\n',...
            ia,params(ia),dParams(ia).du,dParams(ia).dl)];
    end
    legend({sprintf('Data \\pm \\sigma'),lstring(1:end-1)});
        
end

% Plots the weighted fit residual
if options.PlotResiduals
    if options.Plot
        subplot(3,1,3);
    end
    holdstate = ishold;
    plot(xData(:,options.PlotVariable),model(params),'.');
    hold on
    line([min(xData(:,options.PlotVariable)) max(xData(:,options.PlotVariable))],...
        [0 0],'Color',[1 0 0]);
    switch holdstate
        case 0
            hold off
    end
    xlabel('x1');
    ylabel('Residual');
    title('Weighted residual plot');
    if options.Plot
        set(gca,'XLim',get(plotHandle,'XLim'));
%         set(gca,'YLim',get(plotHandle,'YLim'));
    end
end

drawnow
    
    

% --- Internal functions follow ------------------------------------------

    % ----------- model --------------------------------------------------

    % This calls the model function.  It returns the residual at each
    % datum.
    function F = model(x)
        % Handle boundary
        testInBound = between(x,[options.LowerBound; options.UpperBound],1);
        if ~all(testInBound)
            % Return ridiculously large number
            F = sqrt(LARGE_NUM)/(length(x)+1)*ones(length(xData),1);
        else
            % Return the chi-value
            F = (yData - feval(modelFun,x,xData))./(stddev+eps); 
            %eps to avoid problems with zero weights
        end
    end

    % ----------- modelError ---------------------------------------------

    % Used in finding the point-by-point standard deviation
    function F = modelError(x,xbndData)
        F = feval(modelFun,x,xbndData);
    end


    % ---------- calcStdDev ------------------------------------------------

    % Calculates the std. dev. of (f(x_i)-y_i) using finite methods

    function sigma = calcStdDev

        dim = size(xData);
        dm2 = zeros(dim(1),1);
        for ib=1:dim(2)
            xtestu = xData;
            xtestl = xData;
            xtestu(:,ib) = xtestu(:,ib)+dxData(:,ib);
            xtestl(:,ib) = xtestl(:,ib)-dxData(:,ib);
            dmodelu = modelError(params,xtestu);
            dmodell = modelError(params,xtestl);
            dm2 = dm2 + (dmodelu-dmodell).^2/4;
        end
        sigma = sqrt(dyData.^2+dm2);

    end

    %----------------------- modelFix ---------------------------

    % This function is used to fit only one variable to delta chi2 = 1.  It
    % is used with fzero, not lsqnonlin.
    function F = modelFix(var)
        
        if between(var,pVarBnd,1)
            
            pVar = var;
            if length(params>1)
                [pEVarVal,eflag] = trylsq(@modelNFix,pEVarVal,[],[],opEPar);
            end
            if(eflag<=0)
                disp(sprintf(['\tUnexpected termination flag %d in'...
                    ' non-estimating variable minimization\n\tduring'...
                    ' uncertainty estimation'],eflag));
                F=NaN;
            else
                x = reconstruct(pVar);
                F = (chiSquare+1) - sum( model(x).^2 );
            end
            
        % If past the variable-side bound, make F large.  If past the
        % extent-side bound, make F infinitely negative.
        elseif var<min(pVarBnd)
            F = pVarMult*LARGE_NUM;
            eflag = 1;            
        elseif var>max(pVarBnd)
            F = -pVarMult*LARGE_NUM;
            eflag = 1;
        end
            
        % Display iterative function information
        if options.DisplayUncVal
            str1 = sprintf('Flag = %d\tVar = %6.4g\tFVal = %6.4g\tEVal = ',...
                eflag,var,F);
            str2 = sprintf('%g\t',pEVarVal);
            disp([str1 str2]);
        end
    end

    % ------------ reconstruct -------------------------------------------
    % Builds the parameter array from pEVar and pVar

    function x = reconstruct(in)
        for ic=1:length(pEVarInd)
           x(pEVarInd(ic)) = pEVarVal(ic);
        end
        x(pVarIndex) = in;
    end

    % ------------ modelNFix ---------------------------------------------
    % This function is used to fit all but one variable
    function F = modelNFix(x)
        for ig=1:length(pEVarInd)
            xu(pEVarInd(ig)) = x(ig);
        end
        xu(pVarIndex) = pVar;
        F = model(xu);
    end

    % ------------ delta -------------------------------------------------
    % This function finds the variation to delta chi-squared = 1
    % One variable minimizes chi-square+1, while the other variables minimize
    % chi-squared
    function bnd = delta(varyingIndex)
        
        bnd = initializeDParams;
        
        % These are the shared variables for the fitting functions
        pVarIndex = varyingIndex;   % The index of the variable being solved
        
        j=1;                        % Load all the other variables in an array
        for ie=1:length(params)
            if ie~=varyingIndex
                pEVarVal(j)=params(ie);
                pEVarInd(j)=ie;
                j=j+1;
            end
        end
        pEVarValStatic = pEVarVal;  % The non-varying initial array value
        
        pVar = params(pVarIndex);   % The solution is stored here, but must
                                    % start at the minimized value
        
        % This is the original variable value
        pFix = params(pVarIndex);
        
        if ~strcmp(options.Display,'off')
            disp(sprintf('Finding parameter %d lower bound',varyingIndex));
        end

        % Calculate the lower bound
        [bnd.dl,bnd.lVal] = uncMin('lb');
        bnd.dl = pFix-bnd.dl;
        
        % Here we have to reinitiate the variables
        pEVarVal = pEVarValStatic;
        pVar = params(pVarIndex);
        
        % Calculate the upper bound
        if ~strcmp(options.Display,'off')
            disp(sprintf('Finding parameter %d upper bound',varyingIndex));
        end
        
        [bnd.du,bnd.uVal] = uncMin('ub');
        bnd.du = bnd.du-pFix;
        
        bnd.d = mean([bnd.du bnd.dl]);
        
        % This function finds the parameter deviation for delta chi-squared
        % = 1, and then reminimizes the other variables.
        function [bnd,val] = uncMin(lbub)
            
            % Initialize loop variables
            bnd = 0;
            
            % Set the search bounds on the uncertainty
            switch(lbub)
                case 'ub'
                    pVarBnd = [pFix, options.UpperBound(varyingIndex)]; A=1;
                case 'lb'
                    pVarBnd = [options.LowerBound(varyingIndex), pFix]; A=-1;
            end
            pVarMult = A;
            
            opParDev = initializeUncOptions(options);
            % Find the parameter deviation to get delta chi2 = 1
%             [newDelta,fval] = fminsearch(@modelFix,pFix,opParDev);
            [result,fval,exitflag] = fzero(@modelFix,pFix,opParDev);
%             disp(sprintf('Exitflag = %d\tFval = %g',exitflag, fval));
            
            % Assign the result
            bnd = result;
            val = reconstruct(bnd);
                
            if options.DisplayUncVal
                disp(sprintf('Fix = %g\tVar = %g\tChi2 = %g',...
                    pFix,pVar,chiSquare));
            end
        end

    end

    % ----- initializeUncOptions ------------------------------------------

    function uncOp = initializeUncOptions(options)

            % Set a relatively high finishing tolerance
            uncOp = options.UncOptions;
            
    end
        
    % ----- initializeOptions ---------------------------------------------

    function initializeOptions
        % Manufacture the fit options if they don't exist yet
        if ~exist('options','var') | isempty(options)
            options=optimset;
        end
        
        % Plot options
        try
            options.Plot = convertBoolean(options.Plot);
        catch
            options.Plot = 0;
        end
        try
            options.PlotResiduals = convertBoolean(options.PlotResiduals);
        catch
            options.PlotResiduals = 0;
        end
        try
            options.PlotVariable;
        catch
            options.PlotVariable = 1;
        end
        
        % FitUncertainty
        try
            options.FitUncertainty;
        catch
            options.FitUncertainty=1;
            fitUncertaintySpec = 0;
        end
        options.FitUncertainty = convertBoolean(options.FitUncertainty);
        if(length(options.FitUncertainty) ~=1 &...
                length(options.FitUncertainty) ~= length(guess))
            disp(['Badly formed options.FitUncertainty vector, skipping '...
                'uncertainty fit']);
            options.FitUncertainty = zeros(1,length(guess));
        elseif(length(options.FitUncertainty) == 1)
            options.FitUncertainty = ones(1,length(guess))*...
                options.FitUncertainty;
        end
        
        % ErrorsUnknown
        try
            options.ErrorsUnknown;
        catch
            options.ErrorsUnknown=0;
        end
        
        %UncOptions
        try
            options.UncOptions;
        catch
            options.UncOptions=optimset('TolFun',1e-2,...
                'Display','off');
        end
        %DisplayUncVal
        try
            options.DisplayUncVal = convertBoolean(options.DisplayUncVal);
        catch
            options.DisplayUncVal=0;
        end
        %Bounds
        try
            options.LowerBound;
        catch
            options.LowerBound=-Inf*ones(1,length(guess));
        end
        try
            options.UpperBound;
        catch
            options.UpperBound=Inf*ones(1,length(guess));
        end
    end

end

% ----- trylsq ------------------------------------------------------------

% Uses lsqnonlin if it exists
function [sol,exitflag] = trylsq(fun,x0,lb,ub,options)

    if(exist('lsqnonlin','file'))
        [sol,resnorm,residual,exitflag] = lsqnonlin(fun,x0,lb,ub,options);
    else
        [sol,fval,exitflag] = fminsearch(@modelChi2,x0,options);
    end 

    % For use when substituting fminsearch for lsqnonlin
    function F = modelChi2(x)
        F = sum(feval(fun,x).^2);
    end

end

% ----- between -----------------------------------------------------------

% Returns the portion of vector in and out of the range ( as determined by
% the inclusive tag )
function [inner,outer] = between(vector,range,inclusive);

    if ~exist('inclusive','var')
        inclusive = 0;
    end

    if inclusive
        inner = vector<=max(range) & vector>=min(range);
        outer = vector<=min(range) & vector>=max(range);
    else
        inner = vector<max(range) & vector>min(range);
        outer = vector<min(range) & vector>max(range);
    end
    
end

% ----- conditionInput ----------------------------------------------------

function [xData, dxData, yData, dyData] = conditionInput(xin, dxin, yin, dyin,...
    doError)

xData = xin; dxData = dxin; yData = yin; dyData = dyin;

% Force yData and dyData to be column vectors
siy = size(yData);
if siy(1)>1 && siy(2)>1
    error('Y is not a vector');
elseif siy(2)>1
    yData = yData';
    siy = size(yData);
end
if doError
    sidy = size(dyData);
    if sidy(1)>1 && sidy(2)>1
        error('dY is not a vector');
    elseif sidy(2)>1
        dyData = dyData';
        sidy = size(yData);
    end
end
% Transpose xData if it is n x m, not m x n
six = size(xData);
if (siy(1) == six(2) & siy(1) ~= six(1))
    xData = xData';
    six = size(xData);
end
if doError
    sidx = size(dxData);
    if (siy(1) == sidx(2) & siy(1) ~= sidx(1))
        dxData = dxData';
        sidx = size(dxData);
    end
end
% Check that vectors match
if (siy(1) ~= six(1))
    error('X and Y data vectors do not match.')
end
if doError
    if (sidx ~= six) | (sidy ~= siy)
        error('Data vectors do not match error vectors');
    end
end
end

% ----- initializeDParams -------------------------------------------------

function dParamsEmpty = initializeDParams()

dParamsEmpty = struct('dl', NaN, 'lVal', [], 'du', NaN, 'uVal', [],...
    'd',NaN);

end

% ----- conditionBound ----------------------------------------------------

function boundVecOut = conditionBound(boundVecIn)

sib = size(boundVecIn);
if (sib(1)~=1 & sib(2)==1)
    boundVecOut = boundVecIn';
elseif (sib(1)~=1)
    error('Badly formed bound vector');
else
    boundVecOut = boundVecIn;
end
end


% ----- convertBoolean ----------------------------------------------------

% Converts vectors of {>0, <=0} or {'on', 'off'} to
% vectors of {1, 0}

function value = convertBoolean(in)

warning off;
errstr = ['Input vector' sprintf(' %s',in)...
            ' can not be converted to a boolean value'];
warning on;

if iscell(in)
    if all(isnumeric(cell2mat(in)))
        value = cell2mat(in) > 0;
    elseif all(isstrprop(cell2mat(in),'alpha'))
        value = strcmp(in,'on');
    else
        error(errstr);
    end
else
    if all(isnumeric(in))
        value = in > 0;
    elseif isstrprop(in,'alpha');
        switch in
            case 'on'
                value=1;
            case 'off'
                value=0;
            otherwise
                error(errstr);
        end
    else
        error(errstr);
    end
end

end

Contact us at files@mathworks.com