Files
tensor/internal/core/centralsum_kernels_test.go
T
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

365 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package core
import (
"math"
"math/big"
mrand "math/rand/v2"
"testing"
)
// The central-sum evidence file: the accessor-chain Covariance,
// Correlation and Integrate walks against their canonical-block +
// treeSum candidates, both measured against exact big.Float referents
// (512 bits) and timed in one binary.
// covarianceLegacy is the walk the entry point keeps: two accessor
// reads per element folded into one chain.
func covarianceLegacy(a, b []float64, ma, mb float64) float64 {
var cov float64
for i := range a {
cov += (a[i] - ma) * (b[i] - mb)
}
return cov
}
// covarianceBlocks is the candidate: the same per-element arithmetic
// cut into the canonical blocks, one chain partial per block, the
// partials combined through the balanced tree.
func covarianceBlocks(a, b []float64, ma, mb float64) float64 {
n := len(a)
parts := foldParts(n)
if parts == 1 {
return covarianceLegacy(a, b, ma, mb)
}
partials := make([]float64, parts)
for c := range parts {
lo, hi := c*n/parts, (c+1)*n/parts
var acc float64
for i := lo; i < hi; i++ {
acc += (a[i] - ma) * (b[i] - mb)
}
partials[c] = acc
}
return treeSum(partials)
}
// correlationLegacy is the accessor-chain correlation walk.
func correlationLegacy(a, b []float64, ma, mb float64) (num, da2, db2 float64) {
for i := range a {
da := a[i] - ma
dbv := b[i] - mb
num += da * dbv
da2 += da * da
db2 += dbv * dbv
}
return num, da2, db2
}
// correlationBlocks is the candidate: canonical blocks and a tree over
// the partials of each of the three sums.
func correlationBlocks(a, b []float64, ma, mb float64) (num, da2, db2 float64) {
n := len(a)
parts := foldParts(n)
if parts == 1 {
return correlationLegacy(a, b, ma, mb)
}
pn, px, py := make([]float64, parts), make([]float64, parts), make([]float64, parts)
for c := range parts {
lo, hi := c*n/parts, (c+1)*n/parts
var sn, sx, sy float64
for i := lo; i < hi; i++ {
da := a[i] - ma
dbv := b[i] - mb
sn += da * dbv
sx += da * da
sy += dbv * dbv
}
pn[c], px[c], py[c] = sn, sx, sy
}
return treeSum(pn), treeSum(px), treeSum(py)
}
// integrateLegacy is the plain trapezoid chain.
func integrateLegacy(y []float64) float64 {
var total float64
for i := 1; i < len(y); i++ {
total += (y[i-1] + y[i]) / 2
}
return total
}
// integrateBlocks is the candidate: the trapezoid areas cut into the
// canonical blocks over the area indices (n−1 of them), the block
// chains combined through the tree.
func integrateBlocks(y []float64) float64 {
m := len(y) - 1
parts := foldParts(m)
if parts == 1 {
return integrateLegacy(y)
}
partials := make([]float64, parts)
for c := range parts {
lo, hi := c*m/parts, (c+1)*m/parts
var acc float64
for i := lo; i < hi; i++ {
acc += (y[i] + y[i+1]) / 2
}
partials[c] = acc
}
return treeSum(partials)
}
// spreadPair builds a deterministic correlated pair whose magnitudes
// span thirty orders, the shape that cancels a central sum fastest.
func spreadPair(n int, seed uint64) (x, y []float64) {
rng := mrand.New(mrand.NewPCG(seed, seed))
x = make([]float64, n)
y = make([]float64, n)
for i := range x {
mag := math.Pow(10, -15+30*rng.Float64())
if rng.Float64() < 0.5 {
mag = -mag
}
x[i] = mag
y[i] = 3*mag + math.Pow(10, -15+30*rng.Float64())
}
return x, y
}
// exactMeans returns the exact arithmetic means at the given precision.
func exactMeans(x, y []float64, prec uint) (*big.Float, *big.Float) {
sx := new(big.Float).SetPrec(prec)
sy := new(big.Float).SetPrec(prec)
one := new(big.Float).SetPrec(prec)
for i := range x {
sx.Add(sx, new(big.Float).SetPrec(prec).SetFloat64(x[i]))
sy.Add(sy, new(big.Float).SetPrec(prec).SetFloat64(y[i]))
}
one.SetInt(new(big.Int).SetInt64(int64(len(x))))
return new(big.Float).SetPrec(prec).Quo(sx, one), new(big.Float).SetPrec(prec).Quo(sy, one)
}
// TestCentralSumsAccuracy measures the legacy and candidate walks of
// the three central-sum kernels against exact referents.
func TestCentralSumsAccuracy(t *testing.T) {
const prec = 512
n := 1 << 20
x, y := spreadPair(n, 0xBEEF)
exma, exmb := exactMeans(x, y, prec)
maf, _ := exma.Float64()
mbf, _ := exmb.Float64()
// Covariance referent: Σ(x−x̄)(y−ȳ)/(n−1) at full precision.
ref := new(big.Float).SetPrec(prec)
for i := range x {
dx := new(big.Float).SetPrec(prec).Sub(new(big.Float).SetPrec(prec).SetFloat64(x[i]), exma)
dy := new(big.Float).SetPrec(prec).Sub(new(big.Float).SetPrec(prec).SetFloat64(y[i]), exmb)
ref.Add(ref, new(big.Float).SetPrec(prec).Mul(dx, dy))
}
ref.Quo(ref, new(big.Float).SetPrec(prec).SetInt(new(big.Int).SetInt64(int64(n-1))))
refF, _ := ref.Float64()
oldCov := covarianceLegacy(x, y, maf, mbf) / float64(n-1)
newCov := covarianceBlocks(x, y, maf, mbf) / float64(n-1)
scale := math.Max(math.Abs(refF), 1)
t.Logf("covariance n=2^20: exact=%.17g legacy=%.17g (%.3e) blocks=%.17g (%.3e)",
refF, oldCov, math.Abs(oldCov-refF)/scale, newCov, math.Abs(newCov-refF)/scale)
// Correlation referent.
var rnum, rda2, rdb2 = new(big.Float).SetPrec(prec), new(big.Float).SetPrec(prec), new(big.Float).SetPrec(prec)
for i := range x {
dx := new(big.Float).SetPrec(prec).Sub(new(big.Float).SetPrec(prec).SetFloat64(x[i]), exma)
dy := new(big.Float).SetPrec(prec).Sub(new(big.Float).SetPrec(prec).SetFloat64(y[i]), exmb)
rnum.Add(rnum, new(big.Float).SetPrec(prec).Mul(dx, dy))
rda2.Add(rda2, new(big.Float).SetPrec(prec).Mul(dx, dx))
rdb2.Add(rdb2, new(big.Float).SetPrec(prec).Mul(dy, dy))
}
root := new(big.Float).SetPrec(prec).Mul(rda2, rdb2)
root.Sqrt(root)
refR, _ := new(big.Float).SetPrec(prec).Quo(rnum, root).Float64()
on, oda2, odb2 := correlationLegacy(x, y, maf, mbf)
nn, nda2, ndb2 := correlationBlocks(x, y, maf, mbf)
oldR := on / math.Sqrt(oda2*odb2)
newR := nn / math.Sqrt(nda2*ndb2)
t.Logf("correlation n=2^20: exact=%.17g legacy=%.17g (%.3e) blocks=%.17g (%.3e)",
refR, oldR, math.Abs(oldR-refR), newR, math.Abs(newR-refR))
// Integrate referent: the exact trapezoid sum of the same sample.
rngI := mrand.New(mrand.NewPCG(0xDADA, 99))
yv := make([]float64, n)
for i := range yv {
yv[i] = math.Pow(10, -15+30*rngI.Float64())
}
tri := new(big.Float).SetPrec(prec)
for i := 1; i < n; i++ {
a := new(big.Float).SetPrec(prec).SetFloat64(yv[i-1])
b := new(big.Float).SetPrec(prec).SetFloat64(yv[i])
tri.Add(tri, new(big.Float).SetPrec(prec).Quo(new(big.Float).SetPrec(prec).Add(a, b), new(big.Float).SetPrec(prec).SetFloat64(2)))
}
refI, _ := tri.Float64()
oldI := integrateLegacy(yv)
newI := integrateBlocks(yv)
scaleI := math.Max(math.Abs(refI), 1)
t.Logf("integrate n=2^20: exact=%.17g legacy=%.17g (%.3e) blocks=%.17g (%.3e)",
refI, oldI, math.Abs(oldI-refI)/scaleI, newI, math.Abs(newI-refI)/scaleI)
}
// TestCentralSumsProductionPins pins the shipped entry points on the
// spread sample against the exact referent, at tolerances the block
// walk clears by two orders and the plain chain fails: the covariance
// chain erred 1.111e-11 relative here, the block walk 1.642e-14; the
// correlation chain 1.858e-11, the block walk 2.032e-14; the integrate
// chain 5.341e-13, the block walk 3.046e-14.
func TestCentralSumsProductionPins(t *testing.T) {
const prec = 512
n := 1 << 20
x, y := spreadPair(n, 0xBEEF)
xa, _ := FromFloats(x, n)
ya, _ := FromFloats(y, n)
exma, exmb := exactMeans(x, y, prec)
ref := new(big.Float).SetPrec(prec)
for i := range x {
dx := new(big.Float).SetPrec(prec).Sub(new(big.Float).SetPrec(prec).SetFloat64(x[i]), exma)
dy := new(big.Float).SetPrec(prec).Sub(new(big.Float).SetPrec(prec).SetFloat64(y[i]), exmb)
ref.Add(ref, new(big.Float).SetPrec(prec).Mul(dx, dy))
}
ref.Quo(ref, new(big.Float).SetPrec(prec).SetInt(new(big.Int).SetInt64(int64(n-1))))
refF, _ := ref.Float64()
cov, err := Covariance(xa, ya)
if err != nil {
t.Fatal(err)
}
if rel := math.Abs(cov-refF) / math.Abs(refF); rel > 1e-12 {
t.Errorf("covariance relative error %g exceeds 1e-12 (got %v, want %v)", rel, cov, refF)
}
rnum := new(big.Float).SetPrec(prec)
rda2 := new(big.Float).SetPrec(prec)
rdb2 := new(big.Float).SetPrec(prec)
for i := range x {
dx := new(big.Float).SetPrec(prec).Sub(new(big.Float).SetPrec(prec).SetFloat64(x[i]), exma)
dy := new(big.Float).SetPrec(prec).Sub(new(big.Float).SetPrec(prec).SetFloat64(y[i]), exmb)
rnum.Add(rnum, new(big.Float).SetPrec(prec).Mul(dx, dy))
rda2.Add(rda2, new(big.Float).SetPrec(prec).Mul(dx, dx))
rdb2.Add(rdb2, new(big.Float).SetPrec(prec).Mul(dy, dy))
}
root := new(big.Float).SetPrec(prec).Mul(rda2, rdb2)
root.Sqrt(root)
refR, _ := new(big.Float).SetPrec(prec).Quo(rnum, root).Float64()
corr, err := Correlation(xa, ya)
if err != nil {
t.Fatal(err)
}
if math.Abs(corr-refR) > 1e-12 {
t.Errorf("correlation error %g exceeds 1e-12 (got %v, want %v)", math.Abs(corr-refR), corr, refR)
}
yv := make([]float64, n)
rngI := mrand.New(mrand.NewPCG(0xDADA, 99))
for i := range yv {
yv[i] = math.Pow(10, -15+30*rngI.Float64())
}
tri := new(big.Float).SetPrec(prec)
for i := 1; i < n; i++ {
a := new(big.Float).SetPrec(prec).SetFloat64(yv[i-1])
b := new(big.Float).SetPrec(prec).SetFloat64(yv[i])
tri.Add(tri, new(big.Float).SetPrec(prec).Quo(new(big.Float).SetPrec(prec).Add(a, b), new(big.Float).SetPrec(prec).SetFloat64(2)))
}
refI, _ := tri.Float64()
ya2, _ := FromFloats(yv, n)
gotI, err := Integrate(ya2, 1)
if err != nil {
t.Fatal(err)
}
if rel := math.Abs(gotI-refI) / math.Abs(refI); rel > 1e-13 {
t.Errorf("integrate relative error %g exceeds 1e-13 (got %v, want %v)", rel, gotI, refI)
}
}
// TestCentralSumsShortInputsPinEqualBits pins the contract that the
// canonical partition leaves short inputs' bits alone: at and below one
// fold block the block walk is the chain walk, sample for sample.
func TestCentralSumsShortInputsPinEqualBits(t *testing.T) {
rng := mrand.New(mrand.NewPCG(5, 5))
n := 3000
x := make([]float64, n)
y := make([]float64, n)
for i := range x {
x[i] = rng.NormFloat64()
y[i] = rng.NormFloat64()
}
ma, mb := 0.37, -1.2
if covarianceLegacy(x, y, ma, mb) != covarianceBlocks(x, y, ma, mb) {
t.Error("covariance: short-input bits moved")
}
on, oda, odb := correlationLegacy(x, y, ma, mb)
nn, nda, ndb := correlationBlocks(x, y, ma, mb)
if on != nn || oda != nda || odb != ndb {
t.Error("correlation: short-input bits moved")
}
yy := make([]float64, n+1)
for i := range yy {
yy[i] = rng.NormFloat64()
}
if integrateLegacy(yy) != integrateBlocks(yy) {
t.Error("integrate: short-input bits moved")
}
}
// BenchmarkCentralSumsTimes the two walks of each kernel over one
// pair of 2^20 samples.
func BenchmarkCentralSums(b *testing.B) {
n := 1 << 20
x, y := spreadPair(n, 0xBEEF)
ma, mb := 0.37, -1.2
b.Run("covariance/legacy", func(b *testing.B) {
for b.Loop() {
covarianceLegacy(x, y, ma, mb)
}
b.SetBytes(int64(n) * 8)
})
b.Run("covariance/blocks", func(b *testing.B) {
for b.Loop() {
covarianceBlocks(x, y, ma, mb)
}
b.SetBytes(int64(n) * 8)
})
b.Run("correlation/legacy", func(b *testing.B) {
for b.Loop() {
correlationLegacy(x, y, ma, mb)
}
b.SetBytes(int64(n) * 8)
})
b.Run("correlation/blocks", func(b *testing.B) {
for b.Loop() {
correlationBlocks(x, y, ma, mb)
}
b.SetBytes(int64(n) * 8)
})
b.Run("integrate/legacy", func(b *testing.B) {
yy := make([]float64, n)
for i := range yy {
yy[i] = float64(i % 977)
}
for b.Loop() {
integrateLegacy(yy)
}
b.SetBytes(int64(n) * 8)
})
b.Run("integrate/blocks", func(b *testing.B) {
yy := make([]float64, n)
for i := range yy {
yy[i] = float64(i % 977)
}
for b.Loop() {
integrateBlocks(yy)
}
b.SetBytes(int64(n) * 8)
})
}