Files
tensor/stats/view_tail_pin_test.go
petrbalvin af4ee19703
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s
feat: initial release
Assisted-by: GLM 5.3 Flash
2026-09-03 10:00:00 +02:00

192 lines
6.1 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// Pins for the rebased-view walks: a contiguous Slice shares its
// source's storage, so its payload runs past the view's own element
// count, and every raw-payload walk in the package is bounded by the
// visible elements. Each pin holds a view's answer against the same
// computation on fresh arrays carrying nothing but the visible
// elements, with junk parked in the invisible tail: a walk that reads
// one payload slot past its count fails the pin, and a walk that
// writes one panics outright.
package stats
import (
"math"
"testing"
"sourcedock.dev/petrbalvin/tensor/internal/core"
)
// viewTailPool builds a 60-sample pool whose invisible tail, from
// position 40 on, holds values no caller can see, one of them a NaN.
func viewTailPool(t *testing.T) *core.Array {
t.Helper()
pool := make([]float64, 60)
for i := range pool {
pool[i] = float64(i%9) + 1
}
pool[55] = math.NaN() // inside the invisible tail: must be ignored
a, err := core.FromFloats(pool, len(pool))
if err != nil {
t.Fatalf("FromFloats: %v", err)
}
return a
}
// viewTailHalves slices the pool's head into two disjoint views of ten,
// beside fresh arrays holding exactly the visible elements.
func viewTailHalves(t *testing.T) (va, vb, fa, fb *core.Array) {
t.Helper()
pool := viewTailPool(t)
va, err := core.Slice(pool, 0, 0, 10)
if err != nil {
t.Fatalf("Slice: %v", err)
}
vb, err = core.Slice(pool, 0, 10, 20)
if err != nil {
t.Fatalf("Slice: %v", err)
}
visible := pool.RawFloats()[:20]
fa, err = core.FromFloats(visible[:10], 10)
if err != nil {
t.Fatalf("FromFloats: %v", err)
}
fb, err = core.FromFloats(visible[10:], 10)
if err != nil {
t.Fatalf("FromFloats: %v", err)
}
return va, vb, fa, fb
}
// TestViewTailANOVAOneWay pins the analysis of variance on rebased
// views: the group sums, the finite scan and the within sum of squares
// read the visible elements only.
func TestViewTailANOVAOneWay(t *testing.T) {
va, vb, fa, fb := viewTailHalves(t)
f1, p1, err1 := ANOVAOneWay([]*core.Array{va, vb})
f2, p2, err2 := ANOVAOneWay([]*core.Array{fa, fb})
if (err1 == nil) != (err2 == nil) {
t.Fatalf("ANOVAOneWay on views errored %v, on fresh arrays %v", err1, err2)
}
if err1 == nil && (f1 != f2 || p1 != p2) {
t.Fatalf("ANOVAOneWay on views = (%v, %v), fresh arrays answer (%v, %v)", f1, p1, f2, p2)
}
}
// TestViewTailWelchTTest pins Welch's t-test on rebased views: the
// sample mean and variance read the visible elements only.
func TestViewTailWelchTTest(t *testing.T) {
va, vb, fa, fb := viewTailHalves(t)
t1, df1, p1, err1 := WelchTTest(va, vb)
t2, df2, p2, err2 := WelchTTest(fa, fb)
if (err1 == nil) != (err2 == nil) {
t.Fatalf("WelchTTest on views errored %v, on fresh arrays %v", err1, err2)
}
if err1 == nil && (t1 != t2 || df1 != df2 || p1 != p2) {
t.Fatalf("WelchTTest on views = (%v, %v, %v), fresh arrays answer (%v, %v, %v)",
t1, df1, p1, t2, df2, p2)
}
}
// TestViewTailMAD pins the median absolute deviation on a rebased
// view: the deviation walk writes one cell per visible element, never
// one per payload slot.
func TestViewTailMAD(t *testing.T) {
pool := viewTailPool(t)
va, err := core.Slice(pool, 0, 0, 10)
if err != nil {
t.Fatalf("Slice: %v", err)
}
visible := pool.RawFloats()[:10]
fa, err := core.FromFloats(visible, 10)
if err != nil {
t.Fatalf("FromFloats: %v", err)
}
got, err1 := MedianAbsoluteDeviation(va)
want, err2 := MedianAbsoluteDeviation(fa)
if err1 != nil || err2 != nil {
t.Fatalf("MedianAbsoluteDeviation: %v against %v", err1, err2)
}
if got != want {
t.Fatalf("MedianAbsoluteDeviation on a view = %v, fresh array answers %v", got, want)
}
}
// TestViewTailChiSquareGoodnessOfFit pins the goodness-of-fit test on
// rebased views: both frequency walks are bounded by the bin count,
// where an unbounded index into the other payload was a panic before it
// was a wrong statistic.
func TestViewTailChiSquareGoodnessOfFit(t *testing.T) {
pool := viewTailPool(t)
obsV, err := core.Slice(pool, 0, 20, 30)
if err != nil {
t.Fatalf("Slice: %v", err)
}
expV, err := core.Slice(pool, 0, 30, 40)
if err != nil {
t.Fatalf("Slice: %v", err)
}
visible := pool.RawFloats()[:40]
fObs, err := core.FromFloats(visible[20:30], 10)
if err != nil {
t.Fatalf("FromFloats: %v", err)
}
fExp, err := core.FromFloats(visible[30:40], 10)
if err != nil {
t.Fatalf("FromFloats: %v", err)
}
c1, df1, p1, errV := ChiSquareGoodnessOfFit(obsV, expV)
c2, df2, p2, errF := ChiSquareGoodnessOfFit(fObs, fExp)
if (errV == nil) != (errF == nil) {
t.Fatalf("ChiSquareGoodnessOfFit on views errored %v, on fresh arrays %v", errV, errF)
}
if errV == nil && (c1 != c2 || df1 != df2 || p1 != p2) {
t.Fatalf("ChiSquareGoodnessOfFit on views = (%v, %d, %v), fresh arrays answer (%v, %d, %v)",
c1, df1, p1, c2, df2, p2)
}
}
// TestViewTailLinearRegression pins the fit on rebased views: the
// design and response scans refuse only what the visible rows hold, so
// junk in the invisible tail cannot reject a clean fit.
func TestViewTailLinearRegression(t *testing.T) {
design := make([]float64, 40)
for i := range design {
design[i] = float64(i % 7)
}
resp := make([]float64, 20)
for i := range resp {
resp[i] = float64(i%5) * 0.5
}
// Both junk slots sit past their views' visible rows: design row 15
// is invisible to the ten-row design view, response element 15 to
// the ten-row response view.
design[30] = math.NaN()
resp[15] = math.NaN()
designArr, err := core.FromFloats(design, len(design))
if err != nil {
t.Fatalf("FromFloats: %v", err)
}
respArr, err := core.FromFloats(resp, len(resp))
if err != nil {
t.Fatalf("FromFloats: %v", err)
}
design2d, err := core.Reshape(designArr, 20, 2)
if err != nil {
t.Fatalf("Reshape: %v", err)
}
designView, err := core.Slice(design2d, 0, 0, 10)
if err != nil {
t.Fatalf("Slice: %v", err)
}
respView, err := core.Slice(respArr, 0, 0, 10)
if err != nil {
t.Fatalf("Slice: %v", err)
}
if _, err := LinearRegression(designView, respView); err != nil {
t.Fatalf("LinearRegression refused clean visible rows: %v", err)
}
}