mirror of
https://gitlab.com/freepascal.org/lazarus/lazarus.git
synced 2025-04-30 19:03:41 +02:00
372 lines
10 KiB
ObjectPascal
372 lines
10 KiB
ObjectPascal
{ This unit implements a more versatile linear fit than the routines in unit
|
|
ipf of FCL's numlib.
|
|
|
|
Author: Werner Pamler
|
|
}
|
|
|
|
unit TAFitLib;
|
|
|
|
{$mode objfpc}{$H+}
|
|
{$modeswitch nestedprocvars}
|
|
|
|
interface
|
|
|
|
uses
|
|
typ; // data types of numlib
|
|
|
|
type
|
|
TArbFloatArray = array of ArbFloat;
|
|
TArbFloatMatrix = array of array of ArbFloat;
|
|
|
|
TFitFunc = function(x: ArbFloat; Param: Integer): ArbFloat; // is nested;
|
|
|
|
TFitParam = record
|
|
Func: TFitFunc;
|
|
FuncName: String;
|
|
Value: ArbFloat;
|
|
Fixed: Boolean;
|
|
end;
|
|
|
|
TFitParamArray = array of TFitParam;
|
|
|
|
TFitErrCode = (
|
|
fitOK, // everything ok
|
|
fitDimError, // The lengths of the data vectors do not match.
|
|
fitMoreParamsThanValues, // There are more fitting parameters than data values
|
|
fitNoFitParams, // No fit parameters specified
|
|
fitSingular, // Matrix is (nearly) singular
|
|
fitNoBaseFunctions // No user-provided base functions
|
|
);
|
|
|
|
TFitResults = record
|
|
ErrCode: TFitErrCode;
|
|
ParamValues: TArbFloatArray;
|
|
CovarianceMatrix: TArbFloatMatrix;
|
|
N: Integer; // Number of observations
|
|
M: Integer; // Number of fit parameters
|
|
SSR: ArbFloat; // regression sum of squares (yhat - ybar)²
|
|
SSE: ArbFloat; // error sum of squares (yi - yhat)²
|
|
xbar: ArbFloat; // mean x value
|
|
SSx: ArbFloat; // sum of squares (xi - xbar)²
|
|
end;
|
|
|
|
{ for compatibility with TAChart of Lazarus version <= 1.8.x }
|
|
TSimpleFitResults = record
|
|
ErrCode: TFitErrCode;
|
|
ParamValues: TArbFloatArray;
|
|
end;
|
|
|
|
function LinearFit(const x, y, dy: TArbFloatArray;
|
|
FitParams: TFitParamArray): TFitResults;
|
|
|
|
// Some basic fit basis functions for linear least-squares fitting
|
|
function FitBaseFunc_Const({%H-}x: ArbFloat; {%H-}Param: Integer): ArbFloat;
|
|
function FitBaseFunc_Linear(x: ArbFloat; {%H-}Param: Integer): ArbFloat;
|
|
function FitBaseFunc_Square(x: ArbFloat; {%H-}Param: Integer): ArbFloat;
|
|
function FitBaseFunc_Cube(x: ArbFloat; {%H-}Param: Integer): ArbFloat;
|
|
function FitBaseFunc_Poly(x: ArbFloat; Param: Integer): ArbFloat;
|
|
function FitBaseFunc_Sin(x: ArbFloat; Param: Integer): ArbFloat;
|
|
function FitBaseFunc_Cos(x: ArbFloat; Param: Integer): ArbFloat;
|
|
|
|
|
|
implementation
|
|
|
|
uses
|
|
SysUtils,
|
|
math, // IsNaN
|
|
sle, // Solving linear system of equations
|
|
spe, // Incomplete gamma function
|
|
inv; // Inverse matrix
|
|
|
|
function FitBaseFunc_Const(x: ArbFloat; Param: Integer): ArbFloat;
|
|
begin
|
|
Result := 1.0;
|
|
end;
|
|
|
|
function FitBaseFunc_Linear(x: ArbFloat; Param: Integer): ArbFloat;
|
|
begin
|
|
Result := x;
|
|
end;
|
|
|
|
function FitBaseFunc_Square(x: ArbFloat; Param: Integer): ArbFloat;
|
|
begin
|
|
Result := x*x;
|
|
end;
|
|
|
|
function FitBaseFunc_Cube(x: ArbFloat; Param: Integer): ArbFloat;
|
|
begin
|
|
Result := x*x*x;
|
|
end;
|
|
|
|
{ Param is the degree of the polynomial term }
|
|
function FitBaseFunc_Poly(x: ArbFloat; Param: Integer): ArbFloat;
|
|
begin
|
|
Result := 1.0;
|
|
if Param > 0 then
|
|
while Param > 0 do begin
|
|
Result := Result * x;
|
|
dec(Param);
|
|
end
|
|
else
|
|
if Param < 0 then
|
|
while Param < 0 do begin
|
|
Result := Result / x;
|
|
inc(Param);
|
|
end;
|
|
end;
|
|
|
|
function FitBaseFunc_Sin(x: ArbFloat; Param: Integer): ArbFloat;
|
|
begin
|
|
Result := sin(x * Param);
|
|
end;
|
|
|
|
function FitBaseFunc_Cos(x: ArbFloat; Param: Integer): ArbFloat;
|
|
begin
|
|
Result := cos(x * Param);
|
|
end;
|
|
|
|
{ calculates the best-fit value for each y value }
|
|
function CalcBestFitValues(const x, y: TArbFloatArray; n, m: Integer;
|
|
FitParams: TFitParamArray): TArbFloatArray;
|
|
var
|
|
i, j: Integer;
|
|
begin
|
|
SetLength(Result, Length(y));
|
|
for i := 0 to n - 1 do begin
|
|
Result[i] := 0.0;
|
|
for j := 0 to m - 1 do
|
|
Result[i] += fitParams[j].Value * FitParams[j].Func(x[i], j);
|
|
end;
|
|
end;
|
|
|
|
{ Calculates the sum of squares for evaluating the fit statistics }
|
|
procedure CalcSumOfSquares(const y, dy, yhat: TArbFloatArray;
|
|
var SSE, SSR: ArbFloat);
|
|
var
|
|
hasSig: Boolean;
|
|
ybar: Double;
|
|
sig2, totalsig2: Double;
|
|
i, n: Integer;
|
|
begin
|
|
n := Length(y);
|
|
hasSig := (dy <> nil) and (Length(dy) > 0);
|
|
Assert(n = Length(yhat));
|
|
if hasSig then
|
|
Assert(n = Length(dy));
|
|
|
|
// Calculate (weighted) average y data value
|
|
ybar := 0.0;
|
|
if hasSig then begin
|
|
totalsig2 := 0.0;
|
|
for i := 0 to n - 1 do begin
|
|
sig2 := 1.0 / sqr(dy[i]);
|
|
totalsig2 := totalsig2 + sig2;
|
|
ybar := ybar + y[i] * sig2;
|
|
end;
|
|
ybar := ybar / totalsig2;
|
|
end else begin
|
|
for i := 0 to n - 1 do
|
|
ybar := ybar + y[i];
|
|
ybar := ybar / n;
|
|
end;
|
|
|
|
// Calculate sum of squares
|
|
SSR := 0.0;
|
|
SSE := 0.0;
|
|
if hasSig then
|
|
for i := 0 to n - 1 do begin
|
|
sig2 := 1.0 / sqr(dy[i]);
|
|
SSR += sqr(yhat[i] - ybar) * sig2;
|
|
SSE += sqr(y[i] - yhat[i]) * sig2;
|
|
end
|
|
else
|
|
for i := 0 to n - 1 do begin
|
|
SSR += sqr(yhat[i] - ybar);
|
|
SSE += sqr(y[i] - yhat[i]);
|
|
end;
|
|
end;
|
|
|
|
{ Fits a linear combination of the functions defined in FitParams to the data
|
|
arrays provides in x and y. dy contains the error bars of y (std deviation).
|
|
Besides the fit basis functions, the FitParams also contain information
|
|
whether a parameter will be held fixed during fitting ("FitParams[].Fixed").
|
|
In this case, the fixed parameter value is specified in "FitParams[].Values".
|
|
Fit results are returned as a record TFitResults. The field ErrCode is fitOK
|
|
if the fit was successful or contains an error code otherwise. If ErrCode=fitOK
|
|
the TFitResults contain the fitted parameters in field ParamValues[] and their
|
|
standard errors in ParamErros[0], as well as some statistical characterization
|
|
in the other fields (GOF = goodness-of-fit, DOF = degrees of freedom).
|
|
|
|
Ref:
|
|
- Numerical Recipes, Ch 14, Modelling of data, General linear least squares }
|
|
function LinearFit(const x, y, dy: TArbFloatArray;
|
|
FitParams: TFitParamArray): TFitResults;
|
|
var
|
|
alpha: TArbFloatArray;
|
|
beta: TArbFloatArray;
|
|
xx: TArbFloatArray;
|
|
funcs: TArbFloatArray;
|
|
list: Array of Integer;
|
|
ycalc: TArbFloatArray;
|
|
fp: TFitParam;
|
|
n, m, mfit: Integer;
|
|
i, j, k, jk, kj: Integer;
|
|
hasSig: Boolean;
|
|
ym, wt, sig2, chi2: ArbFloat;
|
|
ca: ArbFloat = 0.0;
|
|
term: ArbInt = 0;
|
|
begin
|
|
SetLength(Result.ParamValues, 0);
|
|
SetLength(Result.CovarianceMatrix, 0);
|
|
|
|
// Check parameters
|
|
n := Length(x);
|
|
if n <> Length(y) then begin
|
|
Result.ErrCode := fitDimError;
|
|
exit;
|
|
end;
|
|
hasSig := (dy <> nil) and (Length(dy) > 0);
|
|
if hasSig and (n <> Length(dy)) then begin
|
|
Result.ErrCode := fitDimError;
|
|
exit;
|
|
end;
|
|
|
|
m := Length(FitParams);
|
|
if m < 1 then begin
|
|
Result.ErrCode := fitNoFitParams;
|
|
exit;
|
|
end;
|
|
if m > n then begin
|
|
Result.ErrCode := fitMoreParamsThanValues;
|
|
exit;
|
|
end;
|
|
|
|
// Prepare index list for parameters to be used for fitting
|
|
SetLength(list, m);
|
|
mfit := 0;
|
|
for j := 0 to m - 1 do
|
|
if not FitParams[j].Fixed then begin
|
|
list[mfit] := j;
|
|
inc(mfit);
|
|
end;
|
|
SetLength(list, mfit);
|
|
if mfit = 0 then begin
|
|
Result.ErrCode := fitNoFitParams;
|
|
exit;
|
|
end;
|
|
|
|
// Prepare array for matrix alpha (mfit x mfit) and vector m (length mfit)
|
|
SetLength(alpha, mfit * mfit);
|
|
SetLength(beta, mfit);
|
|
FillChar(alpha[0], mfit * mfit * SizeOf(ArbFloat), 0);
|
|
FillChar(beta[0], mfit * SizeOf(ArbFloat), 0);
|
|
|
|
// Prepare array for values of base functions
|
|
SetLength(funcs, m);
|
|
|
|
// Populate matrix alpha and vector beta
|
|
for i := 0 to n - 1 do begin
|
|
// Calculate values of base functions at x[i]
|
|
for j := 0 to m - 1 do
|
|
funcs[j] := FitParams[j].Func(x[i], j);
|
|
// Subtract the function values of the fixed terms from the constant parameter.
|
|
ym := y[i];
|
|
if mfit < m then
|
|
for j := 0 to m - 1 do begin
|
|
fp := FitParams[j];
|
|
if fp.Fixed then ym := ym - fp.Value * funcs[j];
|
|
end;
|
|
// Prepare factor with standard error
|
|
if hasSig then sig2 := 1.0 / sqr(dy[i]) else sig2 := 1.0;
|
|
// Calculate matrix alpha and vector beta. Note: alpha is symmetric
|
|
for j := 0 to mfit - 1 do begin
|
|
wt := funcs[list[j]] * sig2;
|
|
for k := 0 to j do begin
|
|
jk := j * mfit + k;
|
|
alpha[jk] := alpha[jk] + wt * funcs[list[k]];
|
|
end;
|
|
beta[j] := beta[j] + ym * wt;
|
|
end;
|
|
end;
|
|
// Fill above the diagonal for symmetry
|
|
for j := 1 to mfit-1 do
|
|
for k := 0 to j - 1 do begin
|
|
kj := k * mfit + j;
|
|
jk := j * mfit + k;
|
|
alpha[kj] := alpha[jk];
|
|
end;
|
|
|
|
// Solve equation system
|
|
// alpha * xx = beta, xx contains the fitted parameters
|
|
SetLength(xx, mfit);
|
|
slegen(mfit, mfit, alpha[0], beta[0], xx[0], ca, term);
|
|
|
|
// Check error conditions
|
|
if term = 3 then begin
|
|
// error in input values: mfit < 1
|
|
Result.ErrCode := fitNoFitParams;
|
|
exit;
|
|
// This parameter error already should have been detected. Something is very wrong...
|
|
end;
|
|
|
|
if term = 2 then begin
|
|
// the solution could not have been determined because the matrix is (almost) singular.
|
|
Result.ErrCode := fitSingular;
|
|
exit;
|
|
end;
|
|
|
|
// term = 1 --> success
|
|
// Copy solution to correct index of ParamValues of the FitResult record.
|
|
Result.ErrCode := fitOK;
|
|
Result.N := n;
|
|
Result.M := mfit;
|
|
SetLength(Result.ParamValues, m);
|
|
for j := 0 to m - 1 do
|
|
Result.ParamValues[j] := FitParams[j].Value;
|
|
for j := 0 to mfit-1 do begin
|
|
Result.ParamValues[list[j]] := xx[j];
|
|
FitParams[list[j]].Value := xx[j];
|
|
end;
|
|
|
|
// Calculate sum of squares for statistical analysis
|
|
ycalc := CalcBestFitValues(x, y, n, m, FitParams);
|
|
CalcSumOfSquares(y, dy, ycalc, Result.SSE, Result.SSR);
|
|
SetLength(ycalc, 0);
|
|
chi2 := Result.SSE / (n - mfit);
|
|
|
|
// Calculate inverse of alpha. This is (almost) the variance-covariance matrix
|
|
invgen(mfit, mfit, alpha[0], term);
|
|
// Get variance-covariance matrix.
|
|
for i:=0 to High(alpha) do
|
|
alpha[i] := alpha[i] * chi2;
|
|
|
|
if term = 1 then begin
|
|
// Extract variance/covariance matrix
|
|
SetLength(Result.CovarianceMatrix, m, m);
|
|
for j := 0 to m - 1 do
|
|
for i := 0 to m - 1 do
|
|
Result.CovarianceMatrix[i, j] := NaN;
|
|
for k := 0 to High(alpha) do begin
|
|
j := k div mfit;
|
|
i := k mod mfit;
|
|
Result.CovarianceMatrix[list[i], list[j]] := alpha[k];
|
|
Result.CovarianceMatrix[list[j], list[i]] := alpha[k];
|
|
end;
|
|
end;
|
|
|
|
// Calculate x mean and sum of squares (needed for confidence intervals)
|
|
Result.xbar := 0;
|
|
for i := 0 to n - 1 do Result.xbar += x[i];
|
|
Result.xbar := Result.xbar / n;
|
|
Result.SSx := 0;
|
|
for i := 0 to n - 1 do Result.SSx += sqr(x[i] - Result.xbar);
|
|
|
|
// Clean up
|
|
SetLength(alpha, 0);
|
|
SetLength(beta, 0);
|
|
SetLength(funcs, 0);
|
|
SetLength(xx, 0);
|
|
end;
|
|
|
|
end.
|