TAChart: improper initialization in TFitSeries.PrepareFitParams
Original Reporter info from Mantis: Marcin Wiazowski
-
Reporter name:
Original Reporter info from Mantis: Marcin Wiazowski
- Reporter name:
Description:
TFitSeries can work in the following modes, that can chosen by using a FitEquation property:
fePolynomial, // y = b0 + b1*x + b2*x^2 + ... bn*x^n
feLinear, // y = a + b*x
feExp, // y = a * exp(b * x)
fePower, // y = a * x^b
feCustom // y = b0 + b1*F1(x) + b2*F2(x) + ... bn*Fn(x), where F1(x) .. Fn(x) provided by the user
Let's assume for a moment, that we have FitEquation = fePolynomial, and our interpolated function is:
y = 1 + 2*x + 3*x^2 + 4*x^3
which may be written as:
y = 1*x^0 + 2*x^1 + 3*x^2 + 4*x^3
which may be written as:
y = 1*Power(x,0) + 2*Power(x,1) + 3*Power(x,2) + 4*Power(x,3)
In TFitSeries internals, Power() is encapsulated as FitBaseFunc_Poly():
y = 1*FitBaseFunc_Poly(x,0) + 2*FitBaseFunc_Poly(x,1) + 3*FitBaseFunc_Poly(x,2) + 4*FitBaseFunc_Poly(x,3)
To make a code more universal, a table has been introduced, containing functions to call - it is initialized in a TFitSeries.PrepareFitParams() method as:
FFitParams[0].Func := @FitBaseFunc_Poly // calculates x^0
FFitParams[1].Func := @FitBaseFunc_Poly // calculates x^1
FFitParams[2].Func := @FitBaseFunc_Poly // calculates x^2
FFitParams[3].Func := @FitBaseFunc_Poly // calculates x^3
so we get:
y = 1*FFitParams[0].Func(x,0) + 2*FFitParams[1].Func(x,1) + 3*FFitParams[2].Func(x,2) + 4*FFitParams[3].Func(x,3)
Interestingly, exactly same calculation is used also for feLinear, feExp and fePower modes - only some additional calculations are applied earlier/later.
And, finally, we have the feCustom mode. In this case, we must call a TFitSeries.SetFitBasisFunc() method multiple times, to set FFitParams[x].Func functions as we need. For example, in "tachart\demo\fit\fitdemo.lpr" demo, we have:
FitSeries.SetFitBasisFunc(1, @HarmonicBaseFunc, 'sin(x)');
FitSeries.SetFitBasisFunc(2, @HarmonicBaseFunc, 'sin(3 x)');
FitSeries.SetFitBasisFunc(3, @HarmonicBaseFunc, 'sin(5 x)');
Now, let's reproduce the problem with the attached Reproduce application. There are two identical charts there, each having an identical TFitSeries series - with one exception:
- left chart's series has initial setting FitEquation = fePolynomial,
- right chart's series has initial setting FitEquation = feCustom.
After launching the application, left chart shows a red curve, and right chart shows nothing (this is normal - SetFitBasisFunc() methods have not been called yet).
Now press the "Test" button: it makes both the series identical - both receive same FitEquation = feCustom setting, and same SetFitBasisFunc() calls are made for both of them.
The result is: left chart starts to show a new red curve's shape - but right chart still shows nothing.
This problem can be also reproduced by using the "tachart\demo\fit\fitdemo.lpr" demo:
- to avoid more advanced adjustments in the demo application, at the beginning of TfrmMain.FitCompleteHandler(), just place "exit",
- to avoid more advanced adjustments in the demo application, in TfrmMain.FormCreate(), remove both "FitSeries.FitRange. ... := ..." lines,
- launch the application,
- select the "Fit equation" combo as "Harmonic" (so FitEquation = feCustom mode will be used),
- now you can see a red series on the chart.
Now modify the application, so it will be launched already with the "Harmonic" setting:
- in TfrmMain.FormCreate(), change "cbFitEquation.ItemIndex := 0;" to "cbFitEquation.ItemIndex := 4;" (don't confuse with "cbTestFunction"),
- in Object Inspector, change FitSeries.FitEquation from "fePolynomial" to "feCustom",
- launch the application,
- you will see NOTHING on the chart,
- set the "Fit equation" combo to any other item, and then back to "Harmonic",
- now you can see a red series on the chart - as it should be from the beginning.
Explanation: In non-feCustom modes, all FFitParams[ 0 .. Max ].Func items are initialized internally, in the TFitSeries.PrepareFitParams() method. In feCustom mode, FFitParams[ 1 .. Max ].Func items are initialized by calling SetFitBasisFunc() from the user's code. But what about FFitParams[ 0 ].Func? If the series has been earlier set to some non-feCustom mode, FFitParams[ 0 ].Func is still initialized to @FitBaseFunc_Poly; but if not, FFitParams[ 0 ].Func is not initialized, so chart shows nothing.
So maybe the user should also call SetFitBasisFunc(0, ...)? In this case, the compiler will warn us: "range check error while evaluating constants (0 must be between 1 and 2147483647)". This is because we should never change FFitParams[ 0 ].Func, and it should be always set to @FitBaseFunc_Poly.
This is because FFitParams[0].Func must always return x^0 (which is in fact equal to 1) - this just leaves the first interpolation parameter (a constant value - it's 1 in our example: y = 1* ...) untouched - just as it is required by the interpolation algorithm.
So the solution is: FFitParams[0].Func must be internally initialized to @FitBaseFunc_Poly even when FitEquation = feCustom.
By the way, we can optimize the code a bit: currently, the initialization is:
FFitParams[0].Func := @FitBaseFunc_Poly // calculates x^0
FFitParams[1].Func := @FitBaseFunc_Poly // calculates x^1
FFitParams[2].Func := @FitBaseFunc_Poly // calculates x^2
FFitParams[3].Func := @FitBaseFunc_Poly // calculates x^3
FFitParams[4].Func := @FitBaseFunc_Poly // calculates x^4
FFitParams[5].Func := @FitBaseFunc_Poly // calculates x^5
FFitParams[6].Func := @FitBaseFunc_Poly // calculates x^6
Instead of using FitBaseFunc_Poly to calculate even x^0 or x^1, we can use dedicated (and thus faster) functions:
FFitParams[0].Func := @FitBaseFunc_Const // calculates x^0
FFitParams[1].Func := @FitBaseFunc_Linear // calculates x^1
FFitParams[2].Func := @FitBaseFunc_Square // calculates x^2
FFitParams[3].Func := @FitBaseFunc_Cube // calculates x^3
FFitParams[4].Func := @FitBaseFunc_Poly // calculates x^4
FFitParams[5].Func := @FitBaseFunc_Poly // calculates x^5
FFitParams[6].Func := @FitBaseFunc_Poly // calculates x^6
So, after this optimization, for the FitEquation = feCustom case, FFitParams[0].Func will be internally initialized to @FitBaseFunc_Const.
The attached patch is quite simple; it solves the described problem and also:
- removes one completely outdated comment,
- updates another comment,
- raises an exception if calling SetFitBasisFunc() when FitEquation <> feCustom - in this case, FFitParams[].Func items are initialized internally in TFitSeries.PrepareFitParams() and must not be changed.
After applying the patch and pressing the "Test" button in the Reproduce application, also the right chart shows its series.