| package stepfit |
| |
| import ( |
| "math" |
| "testing" |
| |
| "github.com/stretchr/testify/assert" |
| "github.com/stretchr/testify/require" |
| "go.skia.org/infra/go/vec32" |
| "go.skia.org/infra/perf/go/types" |
| ) |
| |
| const ( |
| // x values are supplied but ignored in all the tests below. |
| x = vec32.MissingDataSentinel |
| |
| // minStdDev is the minimum standard deviation used in all tests. |
| minStdDev = 0.1 |
| ) |
| |
| func TestStepFit_Empty(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{TurningPoint: 0, StepSize: 0, Status: UNINTERESTING, Regression: 0}, |
| GetStepFitAtMid([]float32{}, minStdDev, 20, types.OriginalStep)) |
| } |
| |
| func TestStepFit_SimpleStepUp(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{TurningPoint: 2, StepSize: -2.0412414, Status: HIGH, Regression: -20.412415}, |
| GetStepFitAtMid([]float32{0, 0, 1, 1, 1}, minStdDev, 20, types.OriginalStep)) |
| } |
| |
| func TestStepFit_RealWorldExample(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{LeastSquares: 0.17183419, TurningPoint: 1, StepSize: -2.0251877, Regression: -11.785709, Status: "High"}, |
| GetStepFitAtMid([]float32{608100, |
| 672970, |
| 653180}, minStdDev, 2, types.OriginalStep)) |
| } |
| |
| func TestStepFit_RealWorldExample_SlidingWindow_Comparisons(t *testing.T) { |
| // Visualization of the data generated by Gemini: |
| // ------------------------------------------------------- |
| // | * * |
| // | * * |
| // | * * * * * * * * |
| // | * * * * |
| // | |
| // | * * * |
| // | * * * * * * * * * * |
| // | * * |
| // +-----------------------------------------------------> Time/Index |
| data := []float32{ |
| 90.851, 92.554, 91.193, 91.48547, 90.128, 91.268, 91.349, 92.264, 91.703, 90.687, 91.807, |
| 91.564, 90.507, 91.64, 91.89145, 91.283, 95.879, 94.949, 95.22, 94.745, 95.321, 94.471, 96.363, 95.182, 95.259, |
| 94.179, 94.02346, 96.905, 94.943, 96.395, 95.376, |
| } |
| |
| const ( |
| expectedTurningPointValue float32 = 95.879 |
| interestingThreshold float32 = 2 |
| ) |
| |
| // Result struct to hold what we need to verify |
| type windowResult struct { |
| maxAbsRegression float64 |
| turningPointValue float32 |
| } |
| |
| // Helper to run the sliding window |
| analyzeWindow := func(windowSize int) windowResult { |
| t.Helper() |
| var maxRegression float32 = 0 |
| var maxRegressionIndex = -1 |
| var maxSF *StepFit |
| |
| for i := 0; i+windowSize <= len(data); i++ { |
| window := data[i : i+windowSize] |
| sf := GetStepFitAtMid(window, minStdDev, interestingThreshold, types.CohenStep) |
| |
| if math.Abs(float64(sf.Regression)) > math.Abs(float64(maxRegression)) { |
| maxRegression = sf.Regression |
| maxRegressionIndex = i |
| maxSF = sf |
| } |
| } |
| |
| // Ensure we found a regression (test sanity check) |
| require.NotNil(t, maxSF, "Failed to find any regression for window size %d", windowSize) |
| |
| // Resolve the value at the turning point relative to the window start: |
| // data[start_index + relative_offset] |
| actualValue := data[maxRegressionIndex+maxSF.TurningPoint] |
| |
| return windowResult{ |
| maxAbsRegression: math.Abs(float64(maxRegression)), |
| turningPointValue: actualValue, |
| } |
| } |
| |
| // 1. Establish Baseline (Medium Window Size = 9) |
| // Reference values for Window 9: |
| // [ 0.333, -0.421, -0.761, -0.863, -0.271, 0.923, 0.193, 0.336, 0.197, -1.097, |
| // -1.490, -2.186, -7.053, -1.976, -1.083, -0.613, -0.213, -0.498, -0.473, 0.758, |
| // 0.230, 0.290, -0.289 ] |
| baselineWindowSize := 9 |
| baselineRes := analyzeWindow(baselineWindowSize) |
| |
| assert.Equal(t, expectedTurningPointValue, baselineRes.turningPointValue, |
| "Baseline window (9) failed to identify correct turning point") |
| |
| tests := []struct { |
| name string |
| windowSize int |
| checkFunc func(t *testing.T, res windowResult, baselineReg float64) |
| }{ |
| { |
| name: "Smaller Window (Size 6)", |
| windowSize: 6, |
| checkFunc: func(t *testing.T, res windowResult, baselineReg float64) { |
| // A. Check Invariant: Turning point location must not change |
| assert.Equal(t, expectedTurningPointValue, res.turningPointValue) |
| |
| // B. Check Property: Less smoothing -> Higher Regression magnitude |
| assert.Greater(t, res.maxAbsRegression, baselineReg, |
| "Expected small window (6) to be more sensitive (higher regression) than baseline") |
| |
| // Reference values for Window 6 (for understanding): |
| // [ 0.799, 1.079, 0.953, -1.084, -1.694, -0.566, 0.643, 1.279, -0.138, |
| // 0.014, 0.746, -1.081, -1.179, -1.741, -8.281, -0.797, 0.660, 0.763, |
| // -0.627, -0.448, -1.118, 0.560, 1.704, 0.220, -0.511, -3.510 ] |
| }, |
| }, |
| { |
| name: "Larger Window (Size 15)", |
| windowSize: 15, |
| checkFunc: func(t *testing.T, res windowResult, baselineReg float64) { |
| // A. Check Invariant: Turning point location must not change |
| assert.Equal(t, expectedTurningPointValue, res.turningPointValue) |
| |
| // B. Check Property: More smoothing -> Lower Regression magnitude |
| assert.Less(t, res.maxAbsRegression, baselineReg, |
| "Expected large window (15) to be smoother (lower regression) than baseline") |
| |
| // Reference values for Window 15 (for understanding): |
| // [-0.282, 0.093, 0.002, -0.668, -0.882, -1.134, -1.738, -2.232, -3.043, |
| // -6.551, -2.649, -2.049, -1.384, -0.947, -0.695, -0.554, 0.010 ] |
| }, |
| }, |
| } |
| |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| res := analyzeWindow(tt.windowSize) |
| tt.checkFunc(t, res, baselineRes.maxAbsRegression) |
| }) |
| } |
| } |
| |
| func TestStepFit_SimpleStepDown(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{TurningPoint: 2, StepSize: 2.0412414, Status: LOW, Regression: 20.412415}, |
| GetStepFitAtMid([]float32{1, 1, 0, 0, 0}, minStdDev, 20, types.OriginalStep)) |
| } |
| |
| func TestStepFit_NoStep(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{TurningPoint: 2, StepSize: -1, Status: UNINTERESTING, Regression: -2.7105057e-19, |
| LeastSquares: 3.6893486e+18}, |
| GetStepFitAtMid([]float32{1, 1, 1, 1, 1}, minStdDev, 50, types.OriginalStep)) |
| } |
| |
| func TestStepFit_Absolute_NoStep(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{TurningPoint: 2, StepSize: 0, Status: UNINTERESTING, Regression: 0, LeastSquares: InvalidLeastSquaresError}, |
| GetStepFitAtMid([]float32{1, 2, 1, 2, x}, minStdDev, 1.0, types.AbsoluteStep)) |
| } |
| |
| func TestStepFit_Absolute_StepExactMatch(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{TurningPoint: 2, StepSize: -1, Status: HIGH, Regression: -1, LeastSquares: InvalidLeastSquaresError}, |
| GetStepFitAtMid([]float32{1, 1, 2, 2, x}, minStdDev, 1.0, types.AbsoluteStep)) |
| } |
| |
| func TestStepFit_Absolute_StepTooSmall(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{TurningPoint: 2, StepSize: -0.5, Status: UNINTERESTING, Regression: -0.5, LeastSquares: InvalidLeastSquaresError}, |
| GetStepFitAtMid([]float32{1, 1, 1.5, 1.5, x}, minStdDev, 1.0, types.AbsoluteStep)) |
| } |
| |
| func TestStepFit_Const_NoRegression(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{TurningPoint: 2, StepSize: -1, Status: UNINTERESTING, Regression: -1, LeastSquares: InvalidLeastSquaresError}, |
| GetStepFitAtMid([]float32{0, 0, 1, 0, x}, minStdDev, 2.0, types.Const)) |
| } |
| |
| func TestStepFit_Const_RegressionExactlyAtThreshhold(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{TurningPoint: 2, StepSize: 0, Status: HIGH, Regression: -1, LeastSquares: InvalidLeastSquaresError}, |
| GetStepFitAtMid([]float32{0, 0, 1, 0, x}, minStdDev, 1.0, types.Const)) |
| } |
| |
| func TestStepFit_Const_RegressionAfterApplyingAbs(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{TurningPoint: 2, StepSize: 1, Status: HIGH, Regression: -2, LeastSquares: InvalidLeastSquaresError}, |
| GetStepFitAtMid([]float32{0, 0, -2, 0, x}, minStdDev, 1.0, types.Const)) |
| } |
| |
| func TestStepFit_Percent_NoStep(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{TurningPoint: 2, StepSize: 0, Status: UNINTERESTING, Regression: 0, LeastSquares: InvalidLeastSquaresError}, |
| GetStepFitAtMid([]float32{1, 2, 1, 2, x}, minStdDev, 1.0, types.PercentStep)) |
| } |
| |
| func TestStepFit_Percent_StepExactMatch(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{TurningPoint: 1, StepSize: -1, Status: HIGH, Regression: -1, LeastSquares: InvalidLeastSquaresError}, |
| GetStepFitAtMid([]float32{1, 2, x}, minStdDev, 1.0, types.PercentStep)) |
| } |
| |
| func TestStepFit_Percent_StepTooSmall(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{TurningPoint: 1, StepSize: -0.5, Status: UNINTERESTING, Regression: -0.5, LeastSquares: InvalidLeastSquaresError}, |
| GetStepFitAtMid([]float32{1, 1.5, x}, minStdDev, 1.0, types.PercentStep)) |
| } |
| |
| func TestStepFit_Percent_DefendAgainstNegativeInf(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{TurningPoint: 1, StepSize: -math.MaxFloat32, Status: HIGH, Regression: -math.MaxFloat32, LeastSquares: InvalidLeastSquaresError}, |
| GetStepFitAtMid([]float32{0, 0, 1, 1}, minStdDev, 1.0, types.PercentStep)) |
| } |
| |
| func TestStepFit_Percent_DefendAgainstInf(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{TurningPoint: 1, StepSize: math.MaxFloat32, Status: LOW, Regression: math.MaxFloat32, LeastSquares: InvalidLeastSquaresError}, |
| GetStepFitAtMid([]float32{0, 0, -1, -1}, minStdDev, 1.0, types.PercentStep)) |
| } |
| |
| func TestStepFit_Percent_DefendAgainstNaN(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{TurningPoint: 1, StepSize: 0, Status: UNINTERESTING, Regression: 0, LeastSquares: InvalidLeastSquaresError}, |
| GetStepFitAtMid([]float32{0, 0, 0, 0}, minStdDev, 1.0, types.PercentStep)) |
| } |
| |
| func TestStepFit_Cohen_NoStep(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{TurningPoint: 3, StepSize: -0.1999998, Status: UNINTERESTING, Regression: -0.1999998, LeastSquares: InvalidLeastSquaresError}, |
| GetStepFitAtMid([]float32{1, 1.1, 0.9, 1.02, 1.12, 0.92, x}, minStdDev, 1.0, types.CohenStep)) |
| } |
| |
| func TestStepFit_Cohen_Step(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{TurningPoint: 3, StepSize: -0.1999998, Status: HIGH, Regression: -0.1999998, LeastSquares: InvalidLeastSquaresError}, |
| GetStepFitAtMid([]float32{1, 1.1, 0.9, 1.02, 1.12, 0.92, x}, minStdDev, 0.1, types.CohenStep)) |
| } |
| |
| func TestStepFit_Cohen_StepWithZeroStandardDeviation(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{TurningPoint: 2, StepSize: -10, Status: HIGH, Regression: -10, LeastSquares: InvalidLeastSquaresError}, |
| GetStepFitAtMid([]float32{1, 1, 2, 2, x}, minStdDev, 0.2, types.CohenStep)) |
| } |
| func TestStepFit_Cohen_StepWithLargeStandardDeviation(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{LeastSquares: InvalidLeastSquaresError, TurningPoint: 2, StepSize: -2.828427, Regression: -2.828427, Status: "High"}, |
| GetStepFitAtMid([]float32{1, 2, 3, 4, x}, minStdDev, 0.2, types.CohenStep)) |
| } |
| |
| func TestStepFit_MannWhitneyU_StepHigh(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{LeastSquares: 0, TurningPoint: 4, StepSize: -10, Regression: -0.028571428571428577 /* value copied from stats/utest_test.go:49 */, Status: "High"}, |
| GetStepFitAtMid([]float32{2, 1, 3, 5, 12, 11, 13, 15, x}, minStdDev, 0.05, types.MannWhitneyU)) |
| } |
| |
| func TestStepFit_MannWhitneyU_StepLow(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{LeastSquares: 16, TurningPoint: 4, StepSize: 10, Regression: 0.028571428571428577 /* value copied from stats/utest_test.go:49 */, Status: "Low"}, |
| GetStepFitAtMid([]float32{12, 11, 13, 15, 2, 1, 3, 5, x}, minStdDev, 0.05, types.MannWhitneyU)) |
| } |
| |
| func TestStepFit_MannWhitneyU_UninterestingBecauseInterestingThreshholdTooLow(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{LeastSquares: 16, TurningPoint: 4, StepSize: 10, Regression: 0.028571428571428577 /* value copied from stats/utest_test.go:49 */, Status: "Uninteresting"}, |
| GetStepFitAtMid([]float32{12, 11, 13, 15, 2, 1, 3, 5, x}, minStdDev, 0.01, types.MannWhitneyU)) |
| } |
| |
| func TestStepFit_MannWhitneyU_UninterestingBecauseBothSidesAreEqual(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{LeastSquares: 8, TurningPoint: 4, StepSize: 0, Regression: 1, Status: "Uninteresting"}, |
| GetStepFitAtMid([]float32{2, 1, 3, 5, 2, 1, 3, 5, x}, minStdDev, 0.01, types.MannWhitneyU)) |
| } |
| |
| func TestStepFit_MannWhitneyU_UninterestingBecauseBothSidesAreConstant(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{LeastSquares: 0, TurningPoint: 0, StepSize: 0, Regression: 0, Status: "Uninteresting"}, |
| GetStepFitAtMid([]float32{1, 1, 1, 1, 1, 1, 1, 1, x}, minStdDev, 0.01, types.MannWhitneyU)) |
| } |
| |
| func TestStepFit_MannWhitneyU_UninterestingBecauseNotEnoughData(t *testing.T) { |
| assert.Equal(t, |
| &StepFit{LeastSquares: 0, TurningPoint: 0, StepSize: 0, Regression: 0, Status: "Uninteresting"}, |
| GetStepFitAtMid([]float32{2, 2, x}, minStdDev, 0.01, types.MannWhitneyU)) |
| } |