TAChart: Add monotone cubic Hermite spline as new type of TCubicSplineSeries. Patch by Marcin Wiazowski. Issue #33588.

git-svn-id: trunk@57662 -
This commit is contained in:
wp 2018-04-17 22:15:51 +00:00
parent 00d168199d
commit 51041d4ff2
3 changed files with 211 additions and 54 deletions

View File

@ -27,6 +27,12 @@ interface
uses typ, mdt, dsl, sle, spe;
type
THermiteSplineType = (
hstMonotone // preserves monotonicity of the interpolated function by using
// a Fritsch-Carlson algorithm
);
{ Determine natural cubic spline "s" for data set (x,y), output to (a,d2a)
term=1 success,
=2 failure calculating "s"
@ -47,6 +53,35 @@ Does NOT take source points into account.}
procedure ipfsmm(
n: ArbInt; var x, y, d2s, minv, maxv: ArbFloat; var term: ArbInt);
{Calculates tangents for each data point (d1s), for a given array of input data
points (x,y), by using a selected variant of a Hermite cubic spline interpolation.
Inputs:
hst - algorithm selection
n - highest array index
x[0..n] - array of X values (one value for each data point)
y[0..n] - array of Y values (one value for each data point)
Outputs:
d1s[0..n] - array of tangent values (one value for each data point)
term - status: 1 if function succeeded, 3 if less than two data points given
}
procedure ipfish(hst: THermiteSplineType; n: ArbInt; var x, y, d1s: ArbFloat; var term: ArbInt);
{Calculates interpolated function value for a given array of input data points
(x,y) and tangents for each data point (d1s), for input value t, by using a
Hermite cubic spline interpolation; d1s array can be obtained by calling the
ipfish procedure.
Inputs:
n - highest array index
x[0..n] - array of X values (one value for each data point)
y[0..n] - array of Y values (one value for each data point)
d1s[0..n] - array of tangent values (one value for each data point)
t - input value X
Outputs:
term - status: 1 if function succeeded, 3 if less than two data points given
result - interpolated function value Y
}
function ipfsph(n: ArbInt; var x, y, d1s: ArbFloat; t: ArbFloat; var term: ArbInt): ArbFloat;
{Calculate n-degree polynomal b for dataset (x,y) with n elements
using the least squares method.}
procedure ipfpol(m, n: ArbInt; var x, y, b: ArbFloat; var term: ArbInt);
@ -555,19 +590,22 @@ end; {ipfpol}
procedure ipfisn(n: ArbInt; var x, y, d2s: ArbFloat; var term: ArbInt);
var
s, i : ArbInt;
s, i, L : ArbInt;
p, q : ArbFloat;
px, py, h, b, t : ^arfloat0;
pd2s : ^arfloat1;
ca : Arbfloat = 0.0;
begin
px:=@x; py:=@y; pd2s:=@d2s;
term:=1;
if n < 2
if n < 1
then
begin
term:=3; exit
end; {n<2}
end; {n<1}
if n = 1 then
exit;
px:=@x; py:=@y; pd2s:=@d2s;
s:=sizeof(ArbFloat);
getmem(h, n*s);
getmem(b, (n-1)*s);
@ -585,7 +623,8 @@ begin
begin
q:=1/h^[i-1]; b^[i-2]:=py^[i]*q-py^[i-1]*(p+q)+py^[i-2]*p; p:=q
end;
slegpb(n-1, 1, {2,} t^[1], b^[0], pd2s^[1], ca, term);
if n > 2 then L := 1 else L := 0;
slegpb(n-1, L, {2,} t^[1], b^[0], pd2s^[1], ca, term);
freemem(h, n*s);
freemem(b, (n-1)*s);
freemem(t, 2*(n-1)*s);
@ -600,13 +639,21 @@ var
i, j, m : ArbInt;
d, s3, h, dy : ArbFloat;
begin
i:=1; term:=1;
if n<2
term:=1;
if n<1
then
begin
term:=3; exit
end; {n<2}
end; {n<1}
px:=@x; py:=@y; pd2s:=@d2s;
if n = 1
then
begin
h:=px^[1]-px^[0];
dy:=(py^[1]-py^[0])/h;
ipfspn:=py^[0]+(t-px^[0])*dy
end { n = 1 }
else
if t <= px^[0]
then
begin
@ -716,7 +763,7 @@ var
begin
term:=1;
if n<2 then begin
if n<1 then begin
term:=3;
exit;
end;
@ -725,6 +772,112 @@ begin
MinMaxOnSegment;
end;
procedure ipfish(hst: THermiteSplineType; n: ArbInt; var x, y, d1s: ArbFloat; var term: ArbInt);
var
px, py, pd1s : ^arfloat0;
i : ArbInt;
dks : array of ArbFloat;
begin
px:=@x;
py:=@y;
pd1s:=@d1s;
term:=1;
if n < 1 then
begin
term:=3;
exit;
end;
{Monotone cubic Hermite interpolation}
{See: https://en.wikipedia.org/wiki/Monotone_cubic_interpolation
and: https://en.wikipedia.org/wiki/Cubic_Hermite_spline}
{For each two adjacent data points, calculate tangent of the segment between them}
SetLength(dks,n);
for i:=0 to n-1 do
dks[i]:=(py^[i+1]-py^[i])/(px^[i+1]-px^[i]);
{As proposed by Fritsch and Carlson: For each data point - except the first and
the last one - assign point's tangent (stored in a "d1s" array) as an average
of tangents of the two adjacent segments (this is called 3PD, three-point
difference) - but only if both tangents are either positive (segments are
raising) or negative (segments are falling); in all other cases there is a local
extremum at the data point, or a non-monotonic range begins/continues/ends there,
so spline at this point must be flat to preserve monotonicity - so assign point's
tangent as zero}
for i:=0 to n-2 do
if ((dks[i] > 0) and (dks[i+1] > 0)) or ((dks[i] < 0) and (dks[i+1] < 0)) then
pd1s^[i+1]:=0.5*(dks[i]+dks[i+1])
else
pd1s^[i+1]:=0;
{For the first and the last data point, assign point's tangent as a tangent of
the adjacent segment (this is called one-sided difference)}
pd1s^[0]:=dks[0];
pd1s^[n]:=dks[n-1];
{As proposed by Fritsch and Carlson: Reduce point's tangent if needed, to prevent
overshoot}
for i:=0 to n-1 do
if dks[i] <> 0 then
try
if pd1s^[i]/dks[i] > 3 then
pd1s^[i]:=3*dks[i];
if pd1s^[i+1]/dks[i] > 3 then
pd1s^[i+1]:=3*dks[i];
except
{There may be an exception for dks[i] values that are very close to zero}
pd1s^[i]:=0;
pd1s^[i+1]:=0;
end;
{Addition to the original algorithm: For the first and the last data point,
modify point's tangent in such a way that the cubic Hermite interpolation
polynomial has its inflection point exactly at the data point - so there
will be a smooth transition to the extrapolated part of the graph}
pd1s^[0]:=1.5*dks[0]-0.5*pd1s^[1];
pd1s^[n]:=1.5*dks[n-1]-0.5*pd1s^[n-1];
end; {ipfish}
function ipfsph(n: ArbInt; var x, y, d1s: ArbFloat; t: ArbFloat; var term: ArbInt): ArbFloat;
var
px, py, pd1s : ^arfloat0;
i, j, m : ArbInt;
h : ArbFloat;
begin
i:=1;
term:=1;
if n < 1 then
begin
term:=3;
exit;
end;
px:=@x;
py:=@y;
pd1s:=@d1s;
if t <= px^[0] then
ipfsph:=py^[0]+(t-px^[0])*pd1s^[0]
else
if t >= px^[n] then
ipfsph:=py^[n]+(t-px^[n])*pd1s^[n]
else
begin
i:=0;
j:=n;
while j <> i+1 do
begin
m:=(i+j) div 2;
if t>=px^[m] then
i:=m
else
j:=m;
end; {j}
h:=px^[i+1]-px^[i];
t:=(t-px^[i])/h;
ipfsph:= py^[i]*(1+2*t)*Sqr(1-t) + h*pd1s^[i]*t*Sqr(1-t) + py^[i+1]*Sqr(t)*(3-2*t) + h*pd1s^[i+1]*Sqr(t)*(t-1);
end;
end; {ipfsph}
function p(x, a, z:complex): ArbFloat;
begin
x.sub(a);

View File

@ -13,11 +13,6 @@
<OtherUnitFiles Value="numlib_fix;editors"/>
<UnitOutputDirectory Value="lib\$(TargetCPU)-$(TargetOS)\$(LCLWidgetType)"/>
</SearchPaths>
<CodeGeneration>
<Checks>
<RangeChecks Value="True"/>
</Checks>
</CodeGeneration>
<Linking>
<Debugging>
<UseExternalDbgSyms Value="True"/>

View File

@ -193,43 +193,46 @@ type
end;
TCubicSplineOptions = set of (
csoDrawFewPoints, csoDrawUnorderedX, csoExtrapolateLeft,
csoExtrapolateRight);
csoDrawUnorderedX, csoExtrapolateLeft, csoExtrapolateRight
);
TCubicSplineType = (cstNatural, cstHermiteMonotone);
TCubicSplineSeries = class(TBasicPointSeries)
strict private
FBadDataPen: TBadDataChartPen;
FOptions: TCubicSplineOptions;
FSplineType: TCubicSplineType;
FPen: TChartPen;
FStep: TFuncSeriesStep;
procedure SetPen(AValue: TChartPen);
procedure SetStep(AValue: TFuncSeriesStep);
strict private
type
TSpline = class
public
FOwner: TCubicSplineSeries;
FCoeff, FX, FY: array of ArbFloat;
FIntervals: TIntervalList;
FIsUnorderedX: Boolean;
FStartIndex: Integer;
constructor Create(AOwner: TCubicSplineSeries);
destructor Destroy; override;
function Calculate(AX: Double): Double;
function IsFewPoints: Boolean; inline;
function PrepareCoeffs(
ASource: TCustomChartSource; var AIndex: Integer): Boolean;
procedure PrepareIntervals(AOptions: TCubicSplineOptions);
procedure PrepareIntervals;
end;
var
FSplines: array of TSpline;
procedure FreeSplines;
function IsUnorderedVisible: Boolean; inline;
function IsFewPointsVisible: Boolean; inline;
procedure PrepareCoeffs;
procedure SetBadDataPen(AValue: TBadDataChartPen);
procedure SetOptions(AValue: TCubicSplineOptions);
procedure SetSplineType(AValue: TCubicSplineType);
protected
procedure GetLegendItems(AItems: TChartLegendItems); override;
procedure SourceChanged(ASender: TObject); override;
@ -259,11 +262,13 @@ type
property OnGetPointerStyle;
published
// Used when data is not suitable for drawing cubic spline --
// e.g. points are too few or not ordered by X value.
// e.g. points are not ordered by X value.
property BadDataPen: TBadDataChartPen read FBadDataPen write SetBadDataPen;
property Options: TCubicSplineOptions
read FOptions write SetOptions default [];
property Pen: TChartPen read FPen write SetPen;
property SplineType: TCubicSplineType
read FSplineType write SetSplineType default cstNatural;
property Step: TFuncSeriesStep
read FStep write SetStep default DEF_SPLINE_STEP;
end;
@ -435,7 +440,8 @@ type
implementation
uses
{$IF FPC_FullVersion >= 30101}ipf{$ELSE}ipf_fix{$ENDIF},
ipf_fix,
// {$IF FPC_FullVersion >= 30101}ipf{$ELSE}ipf_fix{$ENDIF},
GraphType, GraphUtil, IntfGraphics, Math, StrUtils, SysUtils,
TAChartStrConsts, TAGeometry, TAGraph, TAMath, TASources;
@ -1123,11 +1129,22 @@ var
ok: Integer = 0;
begin
if IsFewPoints then exit(SafeNaN);
Result := ipfspn(High(FCoeff), FX[0], FY[0], FCoeff[0], AX, ok);
case FOwner.SplineType of
cstNatural:
Result := ipfspn(High(FCoeff), FX[0], FY[0], FCoeff[0], AX, ok);
cstHermiteMonotone:
Result := ipfsph(High(FCoeff), FX[0], FY[0], FCoeff[0], AX, ok);
end;
if ok > 1 then
Result := SafeNaN;
end;
constructor TCubicSplineSeries.TSpline.Create(AOwner: TCubicSplineSeries);
begin
inherited Create;
FOwner := AOwner;
end;
destructor TCubicSplineSeries.TSpline.Destroy;
begin
FreeAndNil(FIntervals);
@ -1136,7 +1153,7 @@ end;
function TCubicSplineSeries.TSpline.IsFewPoints: Boolean;
begin
Result := Length(FX) < 4;
Result := Length(FX) < 2;
end;
function TCubicSplineSeries.TSpline.PrepareCoeffs(
@ -1170,18 +1187,22 @@ begin
if n = 0 then exit(false);
if IsFewPoints then exit(true);
ok := 0;
ipfisn(n - 1, FX[0], FY[0], FCoeff[0], ok);
case FOwner.SplineType of
cstNatural:
ipfisn(n - 1, FX[0], FY[0], FCoeff[0], ok);
cstHermiteMonotone:
ipfish(hstMonotone, n - 1, FX[0], FY[0], FCoeff[0], ok);
end;
Result := ok = 1;
end;
procedure TCubicSplineSeries.TSpline.PrepareIntervals(
AOptions: TCubicSplineOptions);
procedure TCubicSplineSeries.TSpline.PrepareIntervals;
begin
FIntervals := TIntervalList.Create;
try
if not (csoExtrapolateLeft in AOptions) then
if not (csoExtrapolateLeft in FOwner.Options) then
FIntervals.AddRange(NegInfinity, FX[0]);
if not (csoExtrapolateRight in AOptions) then
if not (csoExtrapolateRight in FOwner.Options) then
FIntervals.AddRange(FX[High(FX)], SafeInfinity);
except
FreeAndNil(FIntervals);
@ -1239,23 +1260,6 @@ end;
procedure TCubicSplineSeries.Draw(ADrawer: IChartDrawer);
function DrawFewPoints(ASpline: TSpline): Boolean;
var
pts: TPointArray;
i, lb, ub: Integer;
begin
Result := ASpline.IsFewPoints;
if not Result or not IsFewPointsVisible then exit;
lb := Max(ASpline.FStartIndex, FLoBound);
ub := Min(ASpline.FStartIndex + High(ASpline.FX), FUpBound);
if lb > ub then exit;
SetLength(pts, ub - lb + 1);
for i := lb to ub do
pts[i - lb] := ParentChart.GraphToImage(FGraphPoints[i - FLoBound]);
ADrawer.Pen := BadDataPen;
ADrawer.Polyline(pts, 0, Length(pts));
end;
procedure DrawSpline(ASpline: TSpline);
begin
ADrawer.SetBrushParams(bsClear, clTAColor);
@ -1284,7 +1288,7 @@ begin
PrepareGraphPoints(FChart.CurrentExtent, true);
for s in FSplines do
if not DrawFewPoints(s) then
if not s.IsFewPoints then
DrawSpline(s);
DrawLabels(ADrawer);
@ -1300,6 +1304,8 @@ var
s: TSpline;
begin
Result := inherited Extent;
if SplineType = cstHermiteMonotone then
exit;
if FSplines = nil then
PrepareCoeffs;
if FSplines = nil then exit;
@ -1373,11 +1379,6 @@ begin
end;
end;
function TCubicSplineSeries.IsFewPointsVisible: Boolean;
begin
Result := (csoDrawFewPoints in Options) and BadDataPen.EffVisible;
end;
function TCubicSplineSeries.IsUnorderedVisible: Boolean;
begin
Result := (csoDrawUnorderedX in Options) and BadDataPen.EffVisible;
@ -1390,9 +1391,9 @@ var
begin
FreeSplines;
while i < Source.Count do begin
s := TSpline.Create;
s := TSpline.Create(self);
if s.PrepareCoeffs(Source, i) then begin
s.PrepareIntervals(Options);
s.PrepareIntervals;
SetLength(FSplines, Length(FSplines) + 1);
FSplines[High(FSplines)] := s;
end
@ -1423,6 +1424,14 @@ begin
UpdateParentChart;
end;
procedure TCubicSplineSeries.SetSplineType(AValue: TCubicSplineType);
begin
if FSplineType = AValue then exit;
FSplineType := AValue;
FreeSplines;
UpdateParentChart;
end;
procedure TCubicSplineSeries.SetStep(AValue: TFuncSeriesStep);
begin
if FStep = AValue then exit;