365 lines
12 KiB
Go
365 lines
12 KiB
Go
// 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)
|
|||
|
|
})
|
|||
|
|
}
|