Files
tensor/integrate/cubature_test.go
T

115 lines
3.8 KiB
Go
Raw Normal View History

2026-09-03 10:00:00 +02:00
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package integrate
import (
"math"
"testing"
)
// TestCubatureGaussian pins the 2-D Gaussian against its exact box
// value π·erf(3)²; the infinite-domain π is not what a box integral
// returns.
func TestCubatureGaussian(t *testing.T) {
got, err := IntegrateND(func(x []float64) float64 {
return math.Exp(-x[0]*x[0] - x[1]*x[1])
}, []float64{-3, -3}, []float64{3, 3}, CubatureOptions{Tolerance: 1e-11})
if err != nil {
t.Fatalf("IntegrateND: %v", err)
}
want := math.Pi * math.Erf(3) * math.Erf(3)
if math.Abs(got-want) > 1e-9 {
t.Fatalf("∫∫e^{-r²} = %.12f, want %.12f", got, want)
}
}
// TestCubaturePolynomials pins exactness on products of polynomials.
func TestCubaturePolynomials(t *testing.T) {
got, err := IntegrateND(func(x []float64) float64 {
return x[0] * x[0] * x[1]
}, []float64{0, 0}, []float64{1, 1}, CubatureOptions{})
if err != nil {
t.Fatalf("IntegrateND: %v", err)
}
if math.Abs(got-1.0/6.0) > 1e-13 {
t.Fatalf("∫x²y = %.14f, want 1/6", got)
}
// 3-D volume of the unit cube shifted.
got3, err := IntegrateND(func(x []float64) float64 { return 1 },
[]float64{1, 2, 3}, []float64{3, 5, 7}, CubatureOptions{})
if err != nil {
t.Fatalf("IntegrateND: %v", err)
}
if math.Abs(got3-24) > 1e-12 {
t.Fatalf("volume = %.12f, want 24", got3)
}
}
// TestCubaturePeaked pins adaptivity: a sharp ridge that uniform
// refinement would crawl on, checked against a dense product Simpson.
func TestCubaturePeaked(t *testing.T) {
f := func(x []float64) float64 {
d2 := (x[0] - 0.4) * (x[0] - 0.4)
d2 += (x[1] - 0.6) * (x[1] - 0.6)
return 1 / (0.003 + d2)
}
got, err := IntegrateND(f, []float64{0, 0}, []float64{1, 1}, CubatureOptions{Tolerance: 1e-9})
if err != nil {
t.Fatalf("IntegrateND: %v", err)
}
// Reference: 800×800 composite midpoint product.
const n = 800
h := 1.0 / n
ref := 0.0
for i := range n {
for j := range n {
ref += h * h * f([]float64{(float64(i) + 0.5) * h, (float64(j) + 0.5) * h})
}
}
if math.Abs(got-ref) > 2e-4*ref {
t.Fatalf("peaked integral = %.8f, reference %.8f", got, ref)
}
}
// TestCubatureMatches1D pins the degenerate dimension against the
// one-dimensional adaptive quadrature.
func TestCubatureMatches1D(t *testing.T) {
f := func(x float64) float64 { return math.Exp(-x) * math.Cos(3*x) }
got, err := IntegrateND(func(x []float64) float64 { return f(x[0]) },
[]float64{0}, []float64{5}, CubatureOptions{Tolerance: 1e-12})
if err != nil {
t.Fatalf("IntegrateND: %v", err)
}
ref, _, err := IntegrateFunction(func(x float64) (float64, error) { return f(x), nil },
0, 5, QuadratureOptions{})
if err != nil {
t.Fatalf("IntegrateFunction: %v", err)
}
if math.Abs(got-ref) > 1e-9 {
t.Fatalf("1-D degenerate = %.12f, quadrature says %.12f", got, ref)
}
}
// TestCubatureErrors pins the input gates.
func TestCubatureErrors(t *testing.T) {
if _, err := IntegrateND(func(x []float64) float64 { return 0 },
[]float64{}, []float64{}, CubatureOptions{}); err == nil {
t.Error("empty bounds accepted")
}
if _, err := IntegrateND(func(x []float64) float64 { return 0 },
[]float64{1, 0}, []float64{0, 1}, CubatureOptions{}); err == nil {
t.Error("reversed edge accepted")
}
if _, err := IntegrateND(func(x []float64) float64 { return math.NaN() },
[]float64{0}, []float64{1}, CubatureOptions{}); err == nil {
t.Error("non-finite integrand accepted")
}
// The budget only bites when refinement is actually needed, so the
// integrand must carry an error estimate a constant cannot.
if _, err := IntegrateND(func(x []float64) float64 { return math.Sin(x[0] * x[1]) },
[]float64{0, 0, 0}, []float64{1, 1, 1}, CubatureOptions{MaxEvals: 1}); err == nil {
t.Error("exhausted budget accepted")
}
}