// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package stats import ( "math" "strings" "testing" "sourcedock.dev/petrbalvin/tensor/internal/core" ) // Estimation guards and exactness pins: silent NaN estimates, a // perfect-significance null model, and int64 samples that round // through float64. // TestCovarianceMatrixRejectsNaN: one NaN observation flowed into the // column means and produced an all-NaN matrix with a nil error. func TestCovarianceMatrixRejectsNaN(t *testing.T) { obs, err := core.FromFloats([]float64{1, 2, math.NaN(), 4, 5, 6}, 3, 2) if err != nil { t.Fatal(err) } if _, err := CovarianceMatrix(obs); err == nil || !strings.Contains(err.Error(), "CovarianceMatrix") { t.Fatalf("CovarianceMatrix on a NaN observation: err = %v", err) } if _, err := CorrelationMatrix(obs); err == nil || !strings.Contains(err.Error(), "CorrelationMatrix") { t.Fatalf("CorrelationMatrix on a NaN observation: err = %v", err) } } // TestRegressionInterceptOnly: an intercept-only design skipped the F // block and left FPValue at its zero value, reporting the null model // as maximally significant. func TestRegressionInterceptOnly(t *testing.T) { y, err := core.FromFloats([]float64{2, 4, 6, 8}, 4) if err != nil { t.Fatal(err) } one, err := core.FromFloats([]float64{1, 1, 1, 1}, 4, 1) if err != nil { t.Fatal(err) } for name, run := range map[string]func() (*LinearRegressionResult, error){ "LinearRegression": func() (*LinearRegressionResult, error) { return LinearRegression(one, y) }, "WeightedLinearRegression": func() (*LinearRegressionResult, error) { w, _ := core.FromFloats([]float64{1, 1, 1, 1}, 4) return WeightedLinearRegression(one, y, w) }, } { res, err := run() if err != nil { t.Fatalf("%s: %v", name, err) } if res.FPValue != 1 { t.Fatalf("%s: intercept-only FPValue = %g, want 1", name, res.FPValue) } if res.FStatistic != 0 { t.Fatalf("%s: intercept-only FStatistic = %g, want 0", name, res.FStatistic) } } } // TestHistogramInt64Exact: the samples 2^53, 2^53+1, 2^53+2 all // widened to the same float64 and landed in the wrong bins; the exact // integer path bins them by their true values. func TestHistogramInt64Exact(t *testing.T) { const base = 1 << 53 x, err := core.FromInts([]int64{base + 1, base, base + 2}, 3) if err != nil { t.Fatal(err) } counts, _, err := Histogram(x, 2) if err != nil { t.Fatal(err) } if c0, _ := core.IntAt(counts, 0); c0 != 1 { t.Fatalf("bin 0 count = %d, want 1 (only the minimum)", c0) } if c1, _ := core.IntAt(counts, 1); c1 != 2 { t.Fatalf("bin 1 count = %d, want 2", c1) } } // TestMedianQuantileInt64Exact: the median of {2^53, 2^53+1} came // back as 2^53 although 2^53+0.5 is representable; the same held for // the quantile at 0.5, whose interpolation collapsed on the rounded // pair. func TestMedianQuantileInt64Exact(t *testing.T) { const base = 1 << 53 x, err := core.FromInts([]int64{base, base + 1}, 2) if err != nil { t.Fatal(err) } m, err := Median(x) if err != nil { t.Fatal(err) } if m != base+0.5 { t.Fatalf("Median = %g, want %g", m, base+0.5) } q, err := Quantile(x, []float64{0.5}) if err != nil { t.Fatal(err) } if q.FloatAt(0) != base+0.5 { t.Fatalf("Quantile(0.5) = %g, want %g", q.FloatAt(0), base+0.5) } } // TestGammaUpperOwnErrorName: GammaUpper reported the GammaLower // prefix on the shape refusal. func TestGammaUpperOwnErrorName(t *testing.T) { if _, err := GammaUpper(0, 1); err == nil || !strings.Contains(err.Error(), "GammaUpper:") { t.Fatalf("GammaUpper(0, 1): err = %v", err) } } // TestWelchTTestRejectsNaN: a NaN sample only surfaced when the tail // function rejected the NaN statistic, under its own name. func TestWelchTTestRejectsNaN(t *testing.T) { a, err := core.FromFloats([]float64{math.NaN(), 2, 3}, 3) if err != nil { t.Fatal(err) } b, err := core.FromFloats([]float64{1, 2, 3}, 3) if err != nil { t.Fatal(err) } if _, _, _, err := WelchTTest(a, b); err == nil || !strings.Contains(err.Error(), "WelchTTest") { t.Fatalf("WelchTTest on a NaN sample: err = %v", err) } } // TestMannWhitneyUTieOverflow: a tie block of 2^21+1 observations // overflowed the integer tie term, wrapped it negative and bypassed // the every-observation-tied refusal. func TestMannWhitneyUTieOverflow(t *testing.T) { const n = 1<<21 + 1 vals := make([]float64, n) for i := range vals { vals[i] = 7 } a, err := core.FromFloats(vals, n) if err != nil { t.Fatal(err) } if _, _, err := MannWhitneyU(a, a); err == nil || !strings.Contains(err.Error(), "tied") { t.Fatalf("MannWhitneyU on one giant tie block: err = %v", err) } }