115 lines
3.8 KiB
Go
115 lines
3.8 KiB
Go
// 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")
|
|||
|
|
}
|
|||
|
|
}
|