feat: initial release
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/tensor/internal/core"
|
||||
)
|
||||
|
||||
// Benchmarks for the estimation kernels: the mixture expectation
|
||||
// maximisation sweep, the kernel-density sweep, the windowed extrema
|
||||
// and the histogram passes. Every input is a fixed arithmetic
|
||||
// progression, so two runs of a benchmark measure the same work.
|
||||
|
||||
// benchKernelsSeries builds a deterministic vector of length n: a few
|
||||
// incommensurate frequencies plus a modular jitter, so the sorts and the
|
||||
// window folds meet a spread without a generator. The phase separates
|
||||
// two series of the same length.
|
||||
func benchKernelsSeries(n int, phase float64) []float64 {
|
||||
vals := make([]float64, n)
|
||||
for i := range vals {
|
||||
x := float64(i)
|
||||
vals[i] = math.Sin(0.0017*x+phase)*4 + math.Cos(0.071*x+phase)*2 + float64((i*37)%101)/101
|
||||
}
|
||||
return vals
|
||||
}
|
||||
|
||||
// benchKernelsVector wraps the series as a rank-1 array.
|
||||
func benchKernelsVector(b *testing.B, n int, phase float64) *core.Array {
|
||||
b.Helper()
|
||||
a, err := core.FromFloats(benchKernelsSeries(n, phase), n)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// benchKernelsCloud builds an n-by-d sample of k well-separated clusters
|
||||
// whose jitter follows a fixed congruence: the same rows on every run,
|
||||
// and a cloud the mixture's covariances can factor.
|
||||
func benchKernelsCloud(n, d, k int) []float64 {
|
||||
vals := make([]float64, n*d)
|
||||
for i := range vals {
|
||||
row, col := i/d, i%d
|
||||
jitter := float64((row*2654435761+col*40503)%1000)/1000 - 0.5
|
||||
vals[i] = float64(row%k)*4 + jitter*1.5
|
||||
}
|
||||
return vals
|
||||
}
|
||||
|
||||
// BenchmarkKernelsGaussianMixture measures one full fit: the k-means++
|
||||
// seeding, the Lloyd sweeps and the expectation maximisation sweeps.
|
||||
func BenchmarkKernelsGaussianMixture(b *testing.B) {
|
||||
const n, d, k = 3000, 5, 5
|
||||
data := benchKernelsCloud(n, d, k)
|
||||
x, err := core.FromFloats(data, n, d)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
g := core.NewGenerator(7)
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
if _, err := GaussianMixture(g, x, k); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkKernelsGMMSweep measures the expectation maximisation sweeps
|
||||
// alone: the seeding runs once outside the timed loop, and every
|
||||
// iteration re-copies the seeded parameters, which gmmEM updates in
|
||||
// place.
|
||||
func BenchmarkKernelsGMMSweep(b *testing.B) {
|
||||
const n, d, k = 3000, 5, 5
|
||||
data := benchKernelsCloud(n, d, k)
|
||||
weights, means, covs, err := gmmSeed(core.NewGenerator(7), data, n, d, k)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
w := slices.Clone(weights)
|
||||
m := make([][]float64, k)
|
||||
c := make([][]float64, k)
|
||||
for j := range k {
|
||||
m[j] = slices.Clone(means[j])
|
||||
c[j] = slices.Clone(covs[j])
|
||||
}
|
||||
res, err := gmmEM(data, n, d, k, w, m, c)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.ReportMetric(float64(res.Iterations), "sweeps")
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkKernelsKernelDensity measures the O(n·points) kernel-density
|
||||
// sweep with a fixed bandwidth, so no Silverman pre-pass is timed.
|
||||
func BenchmarkKernelsKernelDensity(b *testing.B) {
|
||||
sample := benchKernelsVector(b, 2048, 0)
|
||||
points := benchKernelsVector(b, 512, 0.5)
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
if _, err := KernelDensity(sample, 0.35, points); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkKernelsRollingMaxWide measures the windowed maximum at half
|
||||
// the series length, the shape that separates a rescan from a deque.
|
||||
func BenchmarkKernelsRollingMaxWide(b *testing.B) {
|
||||
a := benchKernelsVector(b, 16384, 0)
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
if _, err := RollingMax(a, 8192); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkKernelsRollingMinWide is the minimum's counterpart.
|
||||
func BenchmarkKernelsRollingMinWide(b *testing.B) {
|
||||
a := benchKernelsVector(b, 16384, 0)
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
if _, err := RollingMin(a, 8192); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkKernelsHistogram measures the fused scan and the counted
|
||||
// bins over a long sample.
|
||||
func BenchmarkKernelsHistogram(b *testing.B) {
|
||||
a := benchKernelsVector(b, 262144, 0)
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
if _, _, err := Histogram(a, 256); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkKernelsHistogram2D measures the paired binning over a long
|
||||
// sample on both axes.
|
||||
func BenchmarkKernelsHistogram2D(b *testing.B) {
|
||||
const n = 65536
|
||||
x := benchKernelsVector(b, n, 0)
|
||||
y := benchKernelsVector(b, n, 0.5)
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
if _, _, _, err := Histogram2D(x, y, 64, 64); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkKernelsGaussianProcessFit measures one fit over a design the
|
||||
// size a Gaussian-process regression usually runs on: the Gram matrix,
|
||||
// its factorisation, the weights and the posterior at the test points.
|
||||
func BenchmarkKernelsGaussianProcessFit(b *testing.B) {
|
||||
const n, m = 150, 60
|
||||
train := make([]float64, n)
|
||||
test := make([]float64, m)
|
||||
for i := range train {
|
||||
train[i] = float64(i) / float64(n-1)
|
||||
}
|
||||
for i := range test {
|
||||
test[i] = float64(i) / float64(m-1)
|
||||
}
|
||||
trainX, err := core.FromFloats(train, n, 1)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
trainY, err := core.FromFloats(benchKernelsSeries(n, 0), n)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
testX, err := core.FromFloats(test, m, 1)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
kernel, err := SquaredExponentialKernel(0.2)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
if _, err := GaussianProcessRegression(kernel, trainX, trainY, 0.05, testX); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkKernelsRollingMaxScaling reports the monotonic deque against
|
||||
// the series length at a window of half the series: the linear growth
|
||||
// the deque replaced the window rescan with.
|
||||
func BenchmarkKernelsRollingMaxScaling(b *testing.B) {
|
||||
for _, n := range []int{4096, 16384, 65536} {
|
||||
a := benchKernelsVector(b, n, 0)
|
||||
b.Run(fmt.Sprintf("n=%d/w=%d", n, n/2), func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
if _, err := RollingMax(a, n/2); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user