From 6d91d6cab805c68f8b46388e8e3a99cef068e0f8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:06:18 +0000 Subject: [PATCH 1/6] feat: add integrate function to LinearSpline, Step, and CubicSpline Implements definite integral computation for three piecewise interpolation types, addressing part of issue #291. - LinearSpline.integrate: exact integral of piecewise linear function - Step.integrate: exact integral of step function - CubicSpline.integrate: exact integral of piecewise cubic polynomial All three use closed-form antiderivatives for numerical precision. Adds 5 new tests covering known analytical results (y=2x, y=x^2, step). 1198/1198 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/FSharp.Stats/Interpolation.fs | 101 ++++++++++++++++++++++ tests/FSharp.Stats.Tests/Interpolation.fs | 52 +++++++++++ 2 files changed, 153 insertions(+) diff --git a/src/FSharp.Stats/Interpolation.fs b/src/FSharp.Stats/Interpolation.fs index 29f58812..9e258ca6 100644 --- a/src/FSharp.Stats/Interpolation.fs +++ b/src/FSharp.Stats/Interpolation.fs @@ -567,6 +567,34 @@ module Interpolation = let k = leftSegmentIdx lsc.XValues x lsc.C1.[k] + /// + /// Returns the definite integral of the linear spline from xVal1 to xVal2. + /// + /// Linear spline coefficients given as input x values, intersects, and slopes. + /// Lower bound of integration. + /// Upper bound of integration. + /// Definite integral (signed area under the curve) from xVal1 to xVal2. + /// xVal1 and xVal2 should lie within the range of the input x values; values outside the range are extrapolated using the nearest segment. + let integrate (lsc: LinearSplineCoef) xVal1 xVal2 = + if xVal1 = xVal2 then 0. + else + // Integral of segment k from x1 to x2: + // C0[k]*(x2-x1) + C1[k]*((x2-xk)^2 - (x1-xk)^2)/2 + let segmentIntegral k x1 x2 = + let xk = lsc.XValues.[k] + lsc.C0.[k] * (x2 - x1) + lsc.C1.[k] * ((x2 - xk) * (x2 - xk) - (x1 - xk) * (x1 - xk)) / 2. + let k1 = leftSegmentIdx lsc.XValues xVal1 + let k2 = leftSegmentIdx lsc.XValues xVal2 + if k1 = k2 then + segmentIntegral k1 xVal1 xVal2 + else + let firstPart = segmentIntegral k1 xVal1 lsc.XValues.[k1 + 1] + let lastPart = segmentIntegral k2 lsc.XValues.[k2] xVal2 + let middleParts = + [ k1 + 1 .. k2 - 1 ] + |> List.sumBy (fun k -> segmentIntegral k lsc.XValues.[k] lsc.XValues.[k + 1]) + firstPart + middleParts + lastPart + /// /// Module to create linear splines from x,y coordinates. x,y coordinates are interpolated by straight lines between two knots. /// @@ -741,6 +769,36 @@ module Interpolation = let differentiate (lsc: StepCoef) x = 0. + /// + /// Returns the definite integral of the step function from xVal1 to xVal2. + /// + /// Step function coefficients given as input x values and intersects. + /// Lower bound of integration. + /// Upper bound of integration. + /// Definite integral (signed area under the step function) from xVal1 to xVal2. + /// xVal1 and xVal2 should lie within the range of the input x values. + let integrate (lsc: StepCoef) xVal1 xVal2 = + if xVal1 = xVal2 then 0. + else + let n = lsc.XValues.Length + // Find interval index k such that XValues[k] <= x < XValues[k+1], clamped to [0, n-2] + let getInterval x = + if x >= lsc.XValues.[n - 1] then n - 2 + elif x <= lsc.XValues.[0] then 0 + else + lsc.XValues |> Array.findIndex (fun xk -> xk > x) |> fun idx -> idx - 1 + let k1 = getInterval xVal1 + let k2 = getInterval xVal2 + if k1 = k2 then + lsc.C0.[k1] * (xVal2 - xVal1) + else + let firstPart = lsc.C0.[k1] * (lsc.XValues.[k1 + 1] - xVal1) + let lastPart = lsc.C0.[k2] * (xVal2 - lsc.XValues.[k2]) + let middleParts = + [ k1 + 1 .. k2 - 1 ] + |> List.sumBy (fun k -> lsc.C0.[k] * (lsc.XValues.[k + 1] - lsc.XValues.[k])) + firstPart + middleParts + lastPart + /// /// Module to create piecewise cubic polynomials (cubic subsplines) from x,y coordinates. @@ -1741,6 +1799,49 @@ module Interpolation = let getThirdDerivative (coefficients: CubicSplineCoef) x = getDerivative 3 coefficients x + /// + /// Returns the definite integral of the cubic spline from xVal1 to xVal2. + /// + /// Interpolation functions coefficients. + /// Lower bound of integration. + /// Upper bound of integration. + /// Definite integral (signed area under the curve) from xVal1 to xVal2. + /// xVal1 and xVal2 should lie within the range of the input x values; values outside are handled by extrapolating the nearest segment's polynomial. + let integrate (coefficients: CubicSplineCoef) xVal1 xVal2 = + if xVal1 = xVal2 then 0. + else + let sortedX = coefficients.XData |> Seq.sort |> Array.ofSeq + let n = sortedX.Length - 1 // number of intervals + + // Find interval index k such that sortedX[k] <= x < sortedX[k+1], clamped to [0, n-1] + let getInterval x = + if x >= sortedX.[n] then n - 1 + elif x < sortedX.[0] then 0 + else + sortedX |> Array.findIndex (fun xk -> xk > x) |> fun idx -> idx - 1 + + // Antiderivative of the polynomial for interval k, evaluated at x: + // F_k(x) = a*x^4/4 + b*x^3/3 + c*x^2/2 + d*x + let antideriv k x = + let a = coefficients.C0_3.[4 * k + 0] + let b = coefficients.C0_3.[4 * k + 1] + let c = coefficients.C0_3.[4 * k + 2] + let d = coefficients.C0_3.[4 * k + 3] + a * x * x * x * x / 4. + b * x * x * x / 3. + c * x * x / 2. + d * x + + let i1 = getInterval xVal1 + let i2 = getInterval xVal2 + + if i1 = i2 then + antideriv i1 xVal2 - antideriv i1 xVal1 + else + let firstPart = antideriv i1 sortedX.[i1 + 1] - antideriv i1 xVal1 + let lastPart = antideriv i2 xVal2 - antideriv i2 sortedX.[i2] + let middleParts = + [ i1 + 1 .. i2 - 1 ] + |> List.sumBy (fun k -> antideriv k sortedX.[k + 1] - antideriv k sortedX.[k]) + firstPart + middleParts + lastPart + /// /// Hermite cubic splines are defined by the function values and their slopes (first derivatives). If the slopws are unknown, they must be estimated. /// diff --git a/tests/FSharp.Stats.Tests/Interpolation.fs b/tests/FSharp.Stats.Tests/Interpolation.fs index 6a805481..dd9c3140 100644 --- a/tests/FSharp.Stats.Tests/Interpolation.fs +++ b/tests/FSharp.Stats.Tests/Interpolation.fs @@ -142,6 +142,58 @@ let BezierInterpolationTests = +[] +let integrationTests = + testList "Interpolation.integrate" [ + + testCase "LinearSpline.integrate linear function" <| fun () -> + // y = 2x at {0,2,4} => integral [0,4] = [x^2]_0^4 = 16 + let coefs = LinearSpline.interpolate [|0.;2.;4.|] [|0.;4.;8.|] + Expect.floatClose Accuracy.high (LinearSpline.integrate coefs 0. 4.) 16.0 + "integral of y=2x from 0 to 4 should be 16" + // [1,3]: segments [0,2] and [2,4], so partial cross-segment + // ∫[1,3] 2x dx = [x^2]_1^3 = 9 - 1 = 8 + Expect.floatClose Accuracy.high (LinearSpline.integrate coefs 1. 3.) 8.0 + "integral of y=2x from 1 to 3 should be 8" + + testCase "LinearSpline.integrate returns zero for equal bounds" <| fun () -> + let coefs = LinearSpline.interpolate [|0.;1.;2.|] [|1.;2.;3.|] + Expect.floatClose Accuracy.high (LinearSpline.integrate coefs 1. 1.) 0.0 + "integral with equal bounds should be 0" + + testCase "Step.integrate constant segments" <| fun () -> + // y = 2 on [0,1), y = 3 on [1,2), y = 4 on [2,3) + let coefs = Step.interpolate [|0.;1.;2.;3.|] [|2.;3.;4.;5.|] + // ∫[0,3] = 2*1 + 3*1 + 4*1 = 9 + Expect.floatClose Accuracy.high (Step.integrate coefs 0. 3.) 9.0 + "integral of step function from 0 to 3 should be 9" + // ∫[0.5,2.5] = 2*0.5 + 3*1 + 4*0.5 = 1 + 3 + 2 = 6 + Expect.floatClose Accuracy.high (Step.integrate coefs 0.5 2.5) 6.0 + "partial integral of step function from 0.5 to 2.5 should be 6" + + testCase "CubicSpline.integrate quadratic function" <| fun () -> + // Quadratic boundary condition reproduces y=x^2 exactly + let t = vector [| 1.; 2.; 3.; 4. |] + let u = vector [| 1.; 4.; 9.; 16. |] // y = x^2 + let coefs = CubicSpline.interpolate CubicSpline.Quadratic t u + // ∫[1,4] x^2 dx = [x^3/3]_1^4 = 64/3 - 1/3 = 21 + Expect.floatClose Accuracy.high (CubicSpline.integrate coefs 1. 4.) 21.0 + "integral of y=x^2 from 1 to 4 should be 21" + // ∫[1,2] x^2 dx = 8/3 - 1/3 = 7/3 + Expect.floatClose Accuracy.high (CubicSpline.integrate coefs 1. 2.) (7. / 3.) + "integral of y=x^2 from 1 to 2 should be 7/3" + + testCase "CubicSpline.integrate returns zero for equal bounds" <| fun () -> + let t = vector [| 0.; 1.; 2. |] + let u = vector [| 0.; 1.; 4. |] + let coefs = CubicSpline.interpolate CubicSpline.Natural t u + Expect.floatClose Accuracy.high (CubicSpline.integrate coefs 1. 1.) 0.0 + "integral with equal bounds should be 0" + ] + + + + From d7fd78d687c6006bd6a44ff0a510e90164191cee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 13 Apr 2026 13:06:22 +0000 Subject: [PATCH 2/6] ci: trigger checks From 6c6af67fe16483a69e61de3788ed3a7017203a72 Mon Sep 17 00:00:00 2001 From: Anna Weichelt Date: Sat, 11 Jul 2026 10:43:56 +0200 Subject: [PATCH 3/6] add more unit tests --- tests/FSharp.Stats.Tests/Interpolation.fs | 114 +++++++++++++++++++++- 1 file changed, 111 insertions(+), 3 deletions(-) diff --git a/tests/FSharp.Stats.Tests/Interpolation.fs b/tests/FSharp.Stats.Tests/Interpolation.fs index dd9c3140..5ed93de1 100644 --- a/tests/FSharp.Stats.Tests/Interpolation.fs +++ b/tests/FSharp.Stats.Tests/Interpolation.fs @@ -146,7 +146,7 @@ let BezierInterpolationTests = let integrationTests = testList "Interpolation.integrate" [ - testCase "LinearSpline.integrate linear function" <| fun () -> + testCase "LinearSpline.integrate positive linear function" <| fun () -> // y = 2x at {0,2,4} => integral [0,4] = [x^2]_0^4 = 16 let coefs = LinearSpline.interpolate [|0.;2.;4.|] [|0.;4.;8.|] Expect.floatClose Accuracy.high (LinearSpline.integrate coefs 0. 4.) 16.0 @@ -155,13 +155,43 @@ let integrationTests = // ∫[1,3] 2x dx = [x^2]_1^3 = 9 - 1 = 8 Expect.floatClose Accuracy.high (LinearSpline.integrate coefs 1. 3.) 8.0 "integral of y=2x from 1 to 3 should be 8" + // ∫[0.5,1.5] 2x dx = 1.5^2 - 0.5^2 = 2.25 - 0.25 = 2 + Expect.floatClose Accuracy.high (LinearSpline.integrate coefs 0.5 1.5) 2.0 + "integral of y=2x from 0.5 to 1.5 should be 2" + + testCase "LinearSpline.integrate reversed limits" <| fun () -> + let coefs = LinearSpline.interpolate [|0.;2.;4.|] [|0.;4.;8.|] + let forward = LinearSpline.integrate coefs 1. 3. + let reverse = LinearSpline.integrate coefs 3. 1. + + Expect.floatClose Accuracy.high forward (-reverse) + "integrating with reversed limits should negate the result" + + testCase "LinearSpline.integrate is additive" <| fun () -> + let coefs = LinearSpline.interpolate [|0.;2.;4.|] [|0.;4.;8.|] + let whole = LinearSpline.integrate coefs 1. 3. + let partials = LinearSpline.integrate coefs 1. 2. + LinearSpline.integrate coefs 2. 3. + Expect.floatClose Accuracy.high whole partials + "integral should be additive across knot boundaries" testCase "LinearSpline.integrate returns zero for equal bounds" <| fun () -> let coefs = LinearSpline.interpolate [|0.;1.;2.|] [|1.;2.;3.|] Expect.floatClose Accuracy.high (LinearSpline.integrate coefs 1. 1.) 0.0 "integral with equal bounds should be 0" + + testCase "LinearSpline.integrate negative linear function" <| fun () -> + let coefs = LinearSpline.interpolate [|0.;1.|] [|-1.;0.|] + // ∫[0,1] (x-1) dx = -1/2 + Expect.floatClose Accuracy.high (LinearSpline.integrate coefs 0. 1.) -0.5 + "integral of negative linear function should be negative" + + testCase "LinearSpline.integrate across zero crossing" <| fun () -> + let coefs = LinearSpline.interpolate [|0.;1.;2.|] [|-1.;0.;1.|] + // ∫[0,2] (x-1) dx = 0 + Expect.floatClose Accuracy.high (LinearSpline.integrate coefs 0. 2.) 0.0 + "equivalent positive and negative areas should cancel" - testCase "Step.integrate constant segments" <| fun () -> + testCase "Step.integrate constant positive segments" <| fun () -> // y = 2 on [0,1), y = 3 on [1,2), y = 4 on [2,3) let coefs = Step.interpolate [|0.;1.;2.;3.|] [|2.;3.;4.;5.|] // ∫[0,3] = 2*1 + 3*1 + 4*1 = 9 @@ -170,8 +200,47 @@ let integrationTests = // ∫[0.5,2.5] = 2*0.5 + 3*1 + 4*0.5 = 1 + 3 + 2 = 6 Expect.floatClose Accuracy.high (Step.integrate coefs 0.5 2.5) 6.0 "partial integral of step function from 0.5 to 2.5 should be 6" + // ∫[1,2] = 3 * 1 = 3 + Expect.floatClose Accuracy.high (Step.integrate coefs 1. 2.) 3.0 + "integral within exact segment boundaries should be 3" + Expect.floatClose Accuracy.high (Step.integrate coefs 1.5 1.5) 0.0 + "integral over zero-width interval should be zero" + // Uses first interval value for values below x = 0 + // ∫[-1,0] = 2 * 1 = 2 + Expect.floatClose Accuracy.high (Step.integrate coefs -1 0) 2.0 + "integral before the first breakpoint should use first segment value" + + testCase "Step.integrate is additive" <| fun () -> + let coefs = Step.interpolate [|0.;1.;2.;3.|] [|2.;3.;4.;5.|] + let whole = Step.integrate coefs 0.5 2.5 + let partials = (Step.integrate coefs 0.5 1.5) + (Step.integrate coefs 1.5 2.5) + Expect.floatClose Accuracy.high whole partials + "integral should be additive acreoss sub-intervals" + + testCase "Step.integrate reversed limits" <| fun () -> + let coefs = Step.interpolate [|0.;1.;2.;3.|] [|2.;3.;4.;5.|] - testCase "CubicSpline.integrate quadratic function" <| fun () -> + let forward = Step.integrate coefs 0.5 2.5 + let reverse = Step.integrate coefs 2.5 0.5 + + Expect.floatClose Accuracy.high reverse (-forward) + "integrating with reversed limits should negate the result" + + testCase "Step.integrate constant negative segments" <| fun () -> + // y = -2 on [0,1), y = -3 on [1,2) + let coefs = Step.interpolate [|0.;1.;2.|] [|-2.;-3.;-3.|] + //∫[0,2] = (-2)*1 + (-3)*1 = -5 + Expect.floatClose Accuracy.high (Step.integrate coefs 0. 2.) -5.0 + "Integral of negative step function should be negative" + + testCase "Step.integrate across zero crossing" <| fun () -> + // y= -2 on [0,1), y = 2 on [1,2) + let coefs = Step.interpolate [|0.;1.;2.|] [|-2.; 2.; 2.|] + // ∫[0,2] = (-2)*1 + 2*1 = 0 + Expect.floatClose Accuracy.high (Step.integrate coefs 0. 2.) 0.0 + "equivalent positive and negative areas should cancel" + + testCase "CubicSpline.integrate positive quadratic function" <| fun () -> // Quadratic boundary condition reproduces y=x^2 exactly let t = vector [| 1.; 2.; 3.; 4. |] let u = vector [| 1.; 4.; 9.; 16. |] // y = x^2 @@ -182,6 +251,18 @@ let integrationTests = // ∫[1,2] x^2 dx = 8/3 - 1/3 = 7/3 Expect.floatClose Accuracy.high (CubicSpline.integrate coefs 1. 2.) (7. / 3.) "integral of y=x^2 from 1 to 2 should be 7/3" + // ∫[1.5,3.5] x² dx = (3.5³ - 1.5³)/3 + Expect.floatClose Accuracy.high (CubicSpline.integrate coefs 1.5 3.5) ((3.5 ** 3. - 1.5 ** 3.) / 3.) + "integral across several spline intervals should be correct" + + testCase "CubicSpline.integrate is additive" <| fun () -> + let t = vector [| 1.; 2.; 3.; 4. |] + let u = vector [| 1.; 4.; 9.; 16. |] // y = x^2 + let coefs = CubicSpline.interpolate CubicSpline.Quadratic t u + let whole= CubicSpline.integrate coefs 1. 4. + let partials = CubicSpline.integrate coefs 1. 2 + CubicSpline.integrate coefs 2. 3. + CubicSpline.integrate coefs 3. 4. + Expect.floatClose Accuracy.high whole partials + "integral should be additive across sub-intervals" testCase "CubicSpline.integrate returns zero for equal bounds" <| fun () -> let t = vector [| 0.; 1.; 2. |] @@ -189,6 +270,33 @@ let integrationTests = let coefs = CubicSpline.interpolate CubicSpline.Natural t u Expect.floatClose Accuracy.high (CubicSpline.integrate coefs 1. 1.) 0.0 "integral with equal bounds should be 0" + + testCase "CubicSpline.integrate reversed limits" <| fun () -> + let t = vector [| 1.; 2.; 3.; 4. |] + let u = vector [| 1.; 4.; 9.; 16. |] + let coefs = CubicSpline.interpolate CubicSpline.Quadratic t u + + let forward = CubicSpline.integrate coefs 1. 4. + let reverse = CubicSpline.integrate coefs 4. 1. + + Expect.floatClose Accuracy.high forward (-reverse) + "integrating with reversed limits should negate the result" + + testCase "CubicSpline.integrate negative values" <| fun () -> + let t = vector [|1.;2.;3.;4.|] + let u = vector [|-1.;-4.;-9.;-16.|] + let coefs = CubicSpline.interpolate CubicSpline.Quadratic t u + // ∫[1,4] -x² dx = -21 + Expect.floatClose Accuracy.high (CubicSpline.integrate coefs 1. 4.) -21.0 + "integral of negative quadratic should be negative" + + testCase "CubicSpline.integrate positive and negative values" <| fun () -> + let t = vector [|-2.;-1.;0.;1.;2.|] + let u = vector [|-2.;-1.;0.;1.;2.|] + let coefs = CubicSpline.interpolate CubicSpline.Quadratic t u + // ∫[-2,2] x dx = 0 + Expect.floatClose Accuracy.high (CubicSpline.integrate coefs -2. 2.) 0.0 + "positive and negative contributions should cancel" ] From 522c6b80be9ed2291c43a6eff4182ac7fd28149c Mon Sep 17 00:00:00 2001 From: Anna Weichelt Date: Sat, 11 Jul 2026 14:30:08 +0200 Subject: [PATCH 4/6] Fix reversed integration limits issue --- src/FSharp.Stats/Interpolation.fs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/FSharp.Stats/Interpolation.fs b/src/FSharp.Stats/Interpolation.fs index 9e258ca6..c38ae021 100644 --- a/src/FSharp.Stats/Interpolation.fs +++ b/src/FSharp.Stats/Interpolation.fs @@ -575,8 +575,10 @@ module Interpolation = /// Upper bound of integration. /// Definite integral (signed area under the curve) from xVal1 to xVal2. /// xVal1 and xVal2 should lie within the range of the input x values; values outside the range are extrapolated using the nearest segment. - let integrate (lsc: LinearSplineCoef) xVal1 xVal2 = - if xVal1 = xVal2 then 0. + let rec integrate (lsc: LinearSplineCoef) xVal1 xVal2 = + if xVal1 > xVal2 then + - integrate lsc xVal2 xVal1 + elif xVal1 = xVal2 then 0. else // Integral of segment k from x1 to x2: // C0[k]*(x2-x1) + C1[k]*((x2-xk)^2 - (x1-xk)^2)/2 @@ -777,8 +779,10 @@ module Interpolation = /// Upper bound of integration. /// Definite integral (signed area under the step function) from xVal1 to xVal2. /// xVal1 and xVal2 should lie within the range of the input x values. - let integrate (lsc: StepCoef) xVal1 xVal2 = - if xVal1 = xVal2 then 0. + let rec integrate (lsc: StepCoef) xVal1 xVal2 = + if xVal1 > xVal2 then + - integrate lsc xVal2 xVal1 + elif xVal1 = xVal2 then 0. else let n = lsc.XValues.Length // Find interval index k such that XValues[k] <= x < XValues[k+1], clamped to [0, n-2] @@ -1807,8 +1811,10 @@ module Interpolation = /// Upper bound of integration. /// Definite integral (signed area under the curve) from xVal1 to xVal2. /// xVal1 and xVal2 should lie within the range of the input x values; values outside are handled by extrapolating the nearest segment's polynomial. - let integrate (coefficients: CubicSplineCoef) xVal1 xVal2 = - if xVal1 = xVal2 then 0. + let rec integrate (coefficients: CubicSplineCoef) xVal1 xVal2 = + if xVal1 > xVal2 then + - integrate coefficients xVal2 xVal1 + elif xVal1 = xVal2 then 0. else let sortedX = coefficients.XData |> Seq.sort |> Array.ofSeq let n = sortedX.Length - 1 // number of intervals From a1ca67d986f5afa2d8df75d9afe8fec3011e31c0 Mon Sep 17 00:00:00 2001 From: Anna Weichelt Date: Sat, 11 Jul 2026 14:31:56 +0200 Subject: [PATCH 5/6] Change integration function names for uniformity --- src/FSharp.Stats/Interpolation.fs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/FSharp.Stats/Interpolation.fs b/src/FSharp.Stats/Interpolation.fs index c38ae021..fd9a7d3b 100644 --- a/src/FSharp.Stats/Interpolation.fs +++ b/src/FSharp.Stats/Interpolation.fs @@ -575,9 +575,9 @@ module Interpolation = /// Upper bound of integration. /// Definite integral (signed area under the curve) from xVal1 to xVal2. /// xVal1 and xVal2 should lie within the range of the input x values; values outside the range are extrapolated using the nearest segment. - let rec integrate (lsc: LinearSplineCoef) xVal1 xVal2 = + let rec getIntegralBetween (lsc: LinearSplineCoef) xVal1 xVal2 = if xVal1 > xVal2 then - - integrate lsc xVal2 xVal1 + - getIntegralBetween lsc xVal2 xVal1 elif xVal1 = xVal2 then 0. else // Integral of segment k from x1 to x2: @@ -779,9 +779,9 @@ module Interpolation = /// Upper bound of integration. /// Definite integral (signed area under the step function) from xVal1 to xVal2. /// xVal1 and xVal2 should lie within the range of the input x values. - let rec integrate (lsc: StepCoef) xVal1 xVal2 = + let rec getIntegralBetween (lsc: StepCoef) xVal1 xVal2 = if xVal1 > xVal2 then - - integrate lsc xVal2 xVal1 + - getIntegralBetween lsc xVal2 xVal1 elif xVal1 = xVal2 then 0. else let n = lsc.XValues.Length @@ -1811,9 +1811,9 @@ module Interpolation = /// Upper bound of integration. /// Definite integral (signed area under the curve) from xVal1 to xVal2. /// xVal1 and xVal2 should lie within the range of the input x values; values outside are handled by extrapolating the nearest segment's polynomial. - let rec integrate (coefficients: CubicSplineCoef) xVal1 xVal2 = + let rec getIntegralBetween (coefficients: CubicSplineCoef) xVal1 xVal2 = if xVal1 > xVal2 then - - integrate coefficients xVal2 xVal1 + - getIntegralBetween coefficients xVal2 xVal1 elif xVal1 = xVal2 then 0. else let sortedX = coefficients.XData |> Seq.sort |> Array.ofSeq From 4fad8694e8c54c2be6d70950ec53c36fb50f1443 Mon Sep 17 00:00:00 2001 From: Anna Weichelt Date: Sat, 11 Jul 2026 14:41:03 +0200 Subject: [PATCH 6/6] Change function names in tests --- tests/FSharp.Stats.Tests/Interpolation.fs | 96 +++++++++++------------ 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/tests/FSharp.Stats.Tests/Interpolation.fs b/tests/FSharp.Stats.Tests/Interpolation.fs index 5ed93de1..9e46485f 100644 --- a/tests/FSharp.Stats.Tests/Interpolation.fs +++ b/tests/FSharp.Stats.Tests/Interpolation.fs @@ -146,156 +146,156 @@ let BezierInterpolationTests = let integrationTests = testList "Interpolation.integrate" [ - testCase "LinearSpline.integrate positive linear function" <| fun () -> + testCase "LinearSpline.getIntegralBetween positive linear function" <| fun () -> // y = 2x at {0,2,4} => integral [0,4] = [x^2]_0^4 = 16 let coefs = LinearSpline.interpolate [|0.;2.;4.|] [|0.;4.;8.|] - Expect.floatClose Accuracy.high (LinearSpline.integrate coefs 0. 4.) 16.0 + Expect.floatClose Accuracy.high (LinearSpline.getIntegralBetween coefs 0. 4.) 16.0 "integral of y=2x from 0 to 4 should be 16" // [1,3]: segments [0,2] and [2,4], so partial cross-segment // ∫[1,3] 2x dx = [x^2]_1^3 = 9 - 1 = 8 - Expect.floatClose Accuracy.high (LinearSpline.integrate coefs 1. 3.) 8.0 + Expect.floatClose Accuracy.high (LinearSpline.getIntegralBetween coefs 1. 3.) 8.0 "integral of y=2x from 1 to 3 should be 8" // ∫[0.5,1.5] 2x dx = 1.5^2 - 0.5^2 = 2.25 - 0.25 = 2 - Expect.floatClose Accuracy.high (LinearSpline.integrate coefs 0.5 1.5) 2.0 + Expect.floatClose Accuracy.high (LinearSpline.getIntegralBetween coefs 0.5 1.5) 2.0 "integral of y=2x from 0.5 to 1.5 should be 2" - testCase "LinearSpline.integrate reversed limits" <| fun () -> + testCase "LinearSpline.getIntegralBetween reversed limits" <| fun () -> let coefs = LinearSpline.interpolate [|0.;2.;4.|] [|0.;4.;8.|] - let forward = LinearSpline.integrate coefs 1. 3. - let reverse = LinearSpline.integrate coefs 3. 1. + let forward = LinearSpline.getIntegralBetween coefs 1. 3. + let reverse = LinearSpline.getIntegralBetween coefs 3. 1. Expect.floatClose Accuracy.high forward (-reverse) "integrating with reversed limits should negate the result" - testCase "LinearSpline.integrate is additive" <| fun () -> + testCase "LinearSpline.getIntegralBetween is additive" <| fun () -> let coefs = LinearSpline.interpolate [|0.;2.;4.|] [|0.;4.;8.|] - let whole = LinearSpline.integrate coefs 1. 3. - let partials = LinearSpline.integrate coefs 1. 2. + LinearSpline.integrate coefs 2. 3. + let whole = LinearSpline.getIntegralBetween coefs 1. 3. + let partials = LinearSpline.getIntegralBetween coefs 1. 2. + LinearSpline.getIntegralBetween coefs 2. 3. Expect.floatClose Accuracy.high whole partials "integral should be additive across knot boundaries" - testCase "LinearSpline.integrate returns zero for equal bounds" <| fun () -> + testCase "LinearSpline.getIntegralBetween returns zero for equal bounds" <| fun () -> let coefs = LinearSpline.interpolate [|0.;1.;2.|] [|1.;2.;3.|] - Expect.floatClose Accuracy.high (LinearSpline.integrate coefs 1. 1.) 0.0 + Expect.floatClose Accuracy.high (LinearSpline.getIntegralBetween coefs 1. 1.) 0.0 "integral with equal bounds should be 0" - testCase "LinearSpline.integrate negative linear function" <| fun () -> + testCase "LinearSpline.getIntegralBetween negative linear function" <| fun () -> let coefs = LinearSpline.interpolate [|0.;1.|] [|-1.;0.|] // ∫[0,1] (x-1) dx = -1/2 - Expect.floatClose Accuracy.high (LinearSpline.integrate coefs 0. 1.) -0.5 + Expect.floatClose Accuracy.high (LinearSpline.getIntegralBetween coefs 0. 1.) -0.5 "integral of negative linear function should be negative" - testCase "LinearSpline.integrate across zero crossing" <| fun () -> + testCase "LinearSpline.getIntegralBetween across zero crossing" <| fun () -> let coefs = LinearSpline.interpolate [|0.;1.;2.|] [|-1.;0.;1.|] // ∫[0,2] (x-1) dx = 0 - Expect.floatClose Accuracy.high (LinearSpline.integrate coefs 0. 2.) 0.0 + Expect.floatClose Accuracy.high (LinearSpline.getIntegralBetween coefs 0. 2.) 0.0 "equivalent positive and negative areas should cancel" - testCase "Step.integrate constant positive segments" <| fun () -> + testCase "Step.getIntegralBetween constant positive segments" <| fun () -> // y = 2 on [0,1), y = 3 on [1,2), y = 4 on [2,3) let coefs = Step.interpolate [|0.;1.;2.;3.|] [|2.;3.;4.;5.|] // ∫[0,3] = 2*1 + 3*1 + 4*1 = 9 - Expect.floatClose Accuracy.high (Step.integrate coefs 0. 3.) 9.0 + Expect.floatClose Accuracy.high (Step.getIntegralBetween coefs 0. 3.) 9.0 "integral of step function from 0 to 3 should be 9" // ∫[0.5,2.5] = 2*0.5 + 3*1 + 4*0.5 = 1 + 3 + 2 = 6 - Expect.floatClose Accuracy.high (Step.integrate coefs 0.5 2.5) 6.0 + Expect.floatClose Accuracy.high (Step.getIntegralBetween coefs 0.5 2.5) 6.0 "partial integral of step function from 0.5 to 2.5 should be 6" // ∫[1,2] = 3 * 1 = 3 - Expect.floatClose Accuracy.high (Step.integrate coefs 1. 2.) 3.0 + Expect.floatClose Accuracy.high (Step.getIntegralBetween coefs 1. 2.) 3.0 "integral within exact segment boundaries should be 3" - Expect.floatClose Accuracy.high (Step.integrate coefs 1.5 1.5) 0.0 + Expect.floatClose Accuracy.high (Step.getIntegralBetween coefs 1.5 1.5) 0.0 "integral over zero-width interval should be zero" // Uses first interval value for values below x = 0 // ∫[-1,0] = 2 * 1 = 2 - Expect.floatClose Accuracy.high (Step.integrate coefs -1 0) 2.0 + Expect.floatClose Accuracy.high (Step.getIntegralBetween coefs -1 0) 2.0 "integral before the first breakpoint should use first segment value" - testCase "Step.integrate is additive" <| fun () -> + testCase "Step.getIntegralBetween is additive" <| fun () -> let coefs = Step.interpolate [|0.;1.;2.;3.|] [|2.;3.;4.;5.|] - let whole = Step.integrate coefs 0.5 2.5 - let partials = (Step.integrate coefs 0.5 1.5) + (Step.integrate coefs 1.5 2.5) + let whole = Step.getIntegralBetween coefs 0.5 2.5 + let partials = (Step.getIntegralBetween coefs 0.5 1.5) + (Step.getIntegralBetween coefs 1.5 2.5) Expect.floatClose Accuracy.high whole partials "integral should be additive acreoss sub-intervals" - testCase "Step.integrate reversed limits" <| fun () -> + testCase "Step.getIntegralBetween reversed limits" <| fun () -> let coefs = Step.interpolate [|0.;1.;2.;3.|] [|2.;3.;4.;5.|] - let forward = Step.integrate coefs 0.5 2.5 - let reverse = Step.integrate coefs 2.5 0.5 + let forward = Step.getIntegralBetween coefs 0.5 2.5 + let reverse = Step.getIntegralBetween coefs 2.5 0.5 Expect.floatClose Accuracy.high reverse (-forward) "integrating with reversed limits should negate the result" - testCase "Step.integrate constant negative segments" <| fun () -> + testCase "Step.getIntegralBetween constant negative segments" <| fun () -> // y = -2 on [0,1), y = -3 on [1,2) let coefs = Step.interpolate [|0.;1.;2.|] [|-2.;-3.;-3.|] //∫[0,2] = (-2)*1 + (-3)*1 = -5 - Expect.floatClose Accuracy.high (Step.integrate coefs 0. 2.) -5.0 + Expect.floatClose Accuracy.high (Step.getIntegralBetween coefs 0. 2.) -5.0 "Integral of negative step function should be negative" - testCase "Step.integrate across zero crossing" <| fun () -> + testCase "Step.getIntegralBetween across zero crossing" <| fun () -> // y= -2 on [0,1), y = 2 on [1,2) let coefs = Step.interpolate [|0.;1.;2.|] [|-2.; 2.; 2.|] // ∫[0,2] = (-2)*1 + 2*1 = 0 - Expect.floatClose Accuracy.high (Step.integrate coefs 0. 2.) 0.0 + Expect.floatClose Accuracy.high (Step.getIntegralBetween coefs 0. 2.) 0.0 "equivalent positive and negative areas should cancel" - testCase "CubicSpline.integrate positive quadratic function" <| fun () -> + testCase "CubicSpline.getIntegralBetween positive quadratic function" <| fun () -> // Quadratic boundary condition reproduces y=x^2 exactly let t = vector [| 1.; 2.; 3.; 4. |] let u = vector [| 1.; 4.; 9.; 16. |] // y = x^2 let coefs = CubicSpline.interpolate CubicSpline.Quadratic t u // ∫[1,4] x^2 dx = [x^3/3]_1^4 = 64/3 - 1/3 = 21 - Expect.floatClose Accuracy.high (CubicSpline.integrate coefs 1. 4.) 21.0 + Expect.floatClose Accuracy.high (CubicSpline.getIntegralBetween coefs 1. 4.) 21.0 "integral of y=x^2 from 1 to 4 should be 21" // ∫[1,2] x^2 dx = 8/3 - 1/3 = 7/3 - Expect.floatClose Accuracy.high (CubicSpline.integrate coefs 1. 2.) (7. / 3.) + Expect.floatClose Accuracy.high (CubicSpline.getIntegralBetween coefs 1. 2.) (7. / 3.) "integral of y=x^2 from 1 to 2 should be 7/3" // ∫[1.5,3.5] x² dx = (3.5³ - 1.5³)/3 - Expect.floatClose Accuracy.high (CubicSpline.integrate coefs 1.5 3.5) ((3.5 ** 3. - 1.5 ** 3.) / 3.) + Expect.floatClose Accuracy.high (CubicSpline.getIntegralBetween coefs 1.5 3.5) ((3.5 ** 3. - 1.5 ** 3.) / 3.) "integral across several spline intervals should be correct" - testCase "CubicSpline.integrate is additive" <| fun () -> + testCase "CubicSpline.getIntegralBetween is additive" <| fun () -> let t = vector [| 1.; 2.; 3.; 4. |] let u = vector [| 1.; 4.; 9.; 16. |] // y = x^2 let coefs = CubicSpline.interpolate CubicSpline.Quadratic t u - let whole= CubicSpline.integrate coefs 1. 4. - let partials = CubicSpline.integrate coefs 1. 2 + CubicSpline.integrate coefs 2. 3. + CubicSpline.integrate coefs 3. 4. + let whole= CubicSpline.getIntegralBetween coefs 1. 4. + let partials = CubicSpline.getIntegralBetween coefs 1. 2 + CubicSpline.getIntegralBetween coefs 2. 3. + CubicSpline.getIntegralBetween coefs 3. 4. Expect.floatClose Accuracy.high whole partials "integral should be additive across sub-intervals" - testCase "CubicSpline.integrate returns zero for equal bounds" <| fun () -> + testCase "CubicSpline.getIntegralBetween returns zero for equal bounds" <| fun () -> let t = vector [| 0.; 1.; 2. |] let u = vector [| 0.; 1.; 4. |] let coefs = CubicSpline.interpolate CubicSpline.Natural t u - Expect.floatClose Accuracy.high (CubicSpline.integrate coefs 1. 1.) 0.0 + Expect.floatClose Accuracy.high (CubicSpline.getIntegralBetween coefs 1. 1.) 0.0 "integral with equal bounds should be 0" - testCase "CubicSpline.integrate reversed limits" <| fun () -> + testCase "CubicSpline.getIntegralBetween reversed limits" <| fun () -> let t = vector [| 1.; 2.; 3.; 4. |] let u = vector [| 1.; 4.; 9.; 16. |] let coefs = CubicSpline.interpolate CubicSpline.Quadratic t u - let forward = CubicSpline.integrate coefs 1. 4. - let reverse = CubicSpline.integrate coefs 4. 1. + let forward = CubicSpline.getIntegralBetween coefs 1. 4. + let reverse = CubicSpline.getIntegralBetween coefs 4. 1. Expect.floatClose Accuracy.high forward (-reverse) "integrating with reversed limits should negate the result" - testCase "CubicSpline.integrate negative values" <| fun () -> + testCase "CubicSpline.getIntegralBetween negative values" <| fun () -> let t = vector [|1.;2.;3.;4.|] let u = vector [|-1.;-4.;-9.;-16.|] let coefs = CubicSpline.interpolate CubicSpline.Quadratic t u // ∫[1,4] -x² dx = -21 - Expect.floatClose Accuracy.high (CubicSpline.integrate coefs 1. 4.) -21.0 + Expect.floatClose Accuracy.high (CubicSpline.getIntegralBetween coefs 1. 4.) -21.0 "integral of negative quadratic should be negative" - testCase "CubicSpline.integrate positive and negative values" <| fun () -> + testCase "CubicSpline.getIntegralBetween positive and negative values" <| fun () -> let t = vector [|-2.;-1.;0.;1.;2.|] let u = vector [|-2.;-1.;0.;1.;2.|] let coefs = CubicSpline.interpolate CubicSpline.Quadratic t u // ∫[-2,2] x dx = 0 - Expect.floatClose Accuracy.high (CubicSpline.integrate coefs -2. 2.) 0.0 + Expect.floatClose Accuracy.high (CubicSpline.getIntegralBetween coefs -2. 2.) 0.0 "positive and negative contributions should cancel" ]