Files
tensor/internal/core/quasirandom_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

239 lines
7.0 KiB
Go
Raw Permalink 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"
"strconv"
"strings"
"testing"
)
// TestHaltonRadicalInverse pins the van der Corput construction against
// the digit-reflection definition.
func TestHaltonRadicalInverse(t *testing.T) {
cases := []struct {
i, b int
want float64
}{
{1, 2, 0.5},
{2, 2, 0.25},
{3, 2, 0.75},
{4, 2, 0.125},
{5, 2, 0.625},
{1, 3, 1.0 / 3},
{2, 3, 2.0 / 3},
{3, 3, 1.0 / 9},
{7, 3, 5.0 / 9}, // 21₃ reflected is 12₃ = 1/3 + 2/9
{5, 3, 7.0 / 9}, // 12₃ reflected is 21₃ = 2/3 + 1/9
{1, 5, 0.2},
{4, 5, 0.8},
}
for _, c := range cases {
if got := radicalInverse(c.i, c.b); math.Abs(got-c.want) > 1e-15 {
t.Fatalf("radicalInverse(%d, %d) = %g, want %g", c.i, c.b, got, c.want)
}
}
}
// TestHaltonStratification pins the defining property: the first b^k
// points integrate a smooth function far better than random sampling
// would, and ∫x dx through the mean sits at 1/2 to high accuracy.
func TestHaltonStratification(t *testing.T) {
const n = 4096
pts, err := HaltonPoints(n, 2, 0)
if err != nil {
t.Fatalf("HaltonPoints: %v", err)
}
mean := 0.0
for i := range n {
mean += pts.FloatAt(i * 2)
}
mean /= float64(n)
// The base-2 marginal is a permutation of {k/8192}: its mean sits
// at 1/2 to within one point's contribution.
if math.Abs(mean-0.5) > 1.5/float64(n) {
t.Fatalf("mean of base-2 marginal = %.8f, want 1/2 within one point", mean)
}
}
// TestHaltonQuadratureAccuracy pins the Monte Carlo payoff: a Halton
// estimate of a smooth 2-D integral beats the random-sampling error
// scale by orders of magnitude.
func TestHaltonQuadratureAccuracy(t *testing.T) {
const n = 1024
pts, err := HaltonPoints(n, 2, 100)
if err != nil {
t.Fatalf("HaltonPoints: %v", err)
}
est := 0.0
for i := range n {
x := pts.FloatAt(i * 2)
y := pts.FloatAt(i*2 + 1)
est += math.Exp(-x-y*y) / float64(n)
}
// Reference: ∫₀¹∫₀¹ e^{-x-y²} = (1−e^{-1})·sqrt(π)/2·erf(1). The
// Halton error decays like O(log n / n); a thousand points sit
// around 1e-4, where random sampling would scatter at 1/sqrt(n)
// ~ 3e-2.
want := (1 - math.Exp(-1)) * math.Sqrt(math.Pi) / 2 * math.Erf(1)
if math.Abs(est-want) > 5e-4 {
t.Fatalf("Halton integral = %.8f, exact %.8f", est, want)
}
}
// TestHaltonErrors pins the input gates.
func TestHaltonErrors(t *testing.T) {
if _, err := HaltonPoints(10, 0, 0); err == nil {
t.Error("dim 0 accepted")
}
if _, err := HaltonPoints(10, 40, 0); err == nil {
t.Error("dim 40 accepted")
}
if _, err := HaltonPoints(10, 2, -1); err == nil {
t.Error("negative skip accepted")
}
// Zero points is a valid empty request.
empty, err := HaltonPoints(0, 2, 0)
if err != nil || empty.Len() != 0 {
t.Fatalf("n = 0 must give an empty array, got err %v", err)
}
}
// TestSobolCanonicalPoints pins the two dimensions every Sobol table
// agrees on: dimension 1 is Gray-coded van der Corput and the first
// four 2-D points are the canonical square-covering quartet.
func TestSobolCanonicalPoints(t *testing.T) {
want1 := []float64{0.5, 0.75, 0.25, 0.375, 0.875, 0.625, 0.125, 0.1875}
p1, err := SobolPoints(len(want1), 1, 0)
if err != nil {
t.Fatalf("SobolPoints: %v", err)
}
for i, want := range want1 {
if got := p1.FloatAt(i); got != want {
t.Fatalf("dim 1 point %d = %g, want %g", i+1, got, want)
}
}
pts, err := SobolPoints(4, 2, 0)
if err != nil {
t.Fatalf("SobolPoints: %v", err)
}
want2 := [4][2]float64{{0.5, 0.5}, {0.75, 0.25}, {0.25, 0.75}, {0.375, 0.375}}
for p, want := range want2 {
for d := range 2 {
if got := pts.FloatAt(p*2 + d); got != want[d] {
t.Fatalf("point %d dim %d = %g, want %g", p+1, d+1, got, want[d])
}
}
}
}
// TestSobolStratification pins the defining digital property across
// the whole table: in every dimension the 2^m points X_0..X_{2^m-1}
// (the origin included, since it carries the zero cell) hit each of
// the 2^m equal subintervals exactly once, which holds only if the
// direction numbers are linearly independent over GF(2).
func TestSobolStratification(t *testing.T) {
for _, b := range []int{4, 8} {
count := 1 << b
for dim := 1; dim <= len(sobolTable); dim++ {
// X_1..X_{count-1}; X_0 is the origin and fills cell 0.
pts, err := SobolPoints(count-1, dim, 0)
if err != nil {
t.Fatalf("SobolPoints dim %d: %v", dim, err)
}
for d := range dim {
seen := make([]bool, count)
seen[0] = true
for p := range count - 1 {
cell := int(pts.FloatAt(p*dim+d) * float64(count))
if cell == count {
cell = count - 1 // a value of 1.0 would be out of range
}
if seen[cell] {
t.Fatalf("dim %d, coordinate %d: subinterval %d hit twice in the first %d points", dim, d, cell, count)
}
seen[cell] = true
}
}
}
}
}
// TestSobolSkip: skipping must be equivalent to generating more points
// and discarding the leading ones, for every coordinate.
func TestSobolSkip(t *testing.T) {
full, err := SobolPoints(12, 3, 0)
if err != nil {
t.Fatalf("SobolPoints: %v", err)
}
tail, err := SobolPoints(4, 3, 8)
if err != nil {
t.Fatalf("SobolPoints: %v", err)
}
for p := range 4 {
for d := range 3 {
got := tail.FloatAt(p*3 + d)
want := full.FloatAt((p+8)*3 + d)
if got != want {
t.Fatalf("skipped point %d dim %d = %g, want %g", p, d+1, got, want)
}
}
}
}
// TestSobolQuadratureAccuracy: the same integral TestHaltonQuadrature
// pins, at the same sample count and bound. Sobol carries no
// early-point correlation to skip away, so the sample runs from the
// start, where the digital structure is densest.
func TestSobolQuadratureAccuracy(t *testing.T) {
const n = 1024
pts, err := SobolPoints(n, 2, 0)
if err != nil {
t.Fatalf("SobolPoints: %v", err)
}
est := 0.0
for i := range n {
x := pts.FloatAt(i * 2)
y := pts.FloatAt(i*2 + 1)
est += math.Exp(-x-y*y) / float64(n)
}
want := (1 - math.Exp(-1)) * math.Sqrt(math.Pi) / 2 * math.Erf(1)
if math.Abs(est-want) > 5e-4 {
t.Fatalf("Sobol integral = %.8f, exact %.8f", est, want)
}
}
// TestSobolErrors pins the input gates.
func TestSobolErrors(t *testing.T) {
if _, err := SobolPoints(10, 0, 0); err == nil {
t.Error("dim 0 accepted")
}
if _, err := SobolPoints(10, 41, 0); err == nil {
t.Error("dim 41 accepted")
}
if _, err := SobolPoints(-1, 2, 0); err == nil {
t.Error("negative n accepted")
}
if _, err := SobolPoints(10, 2, -1); err == nil {
t.Error("negative skip accepted")
}
empty, err := SobolPoints(0, 2, 0)
if err != nil || empty.Len() != 0 {
t.Fatalf("n = 0 must give an empty array, got err %v", err)
}
// The period guard needs indices a 32-bit int cannot express.
if strconv.IntSize >= 64 {
if _, err := SobolPoints(2, 1, (1<<32)-1); err == nil ||
!strings.Contains(err.Error(), "sequence period") {
t.Errorf("period wrap accepted: %v", err)
}
// The last legal index is still fine.
if _, err := SobolPoints(1, 1, (1<<32)-2); err != nil {
t.Errorf("last legal index refused: %v", err)
}
}
}