// Copyright (c) 2026 Petr Balvín (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") } }