190 lines
6.8 KiB
Go
190 lines
6.8 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: MIT
|
||
|
|
|
||
|
|
package integrate
|
||
|
|
|
||
|
|
import (
|
||
|
|
"math"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"sourcedock.dev/petrbalvin/tensor/internal/base"
|
||
|
|
)
|
||
|
|
|
||
|
|
// intFilon closed forms: the antiderivatives the referents come from.
|
||
|
|
// intFilon1 integrates 1·cos(kx) and 1·sin(kx); intFilonX integrates x
|
||
|
|
// against the same kernels; intFilonExp integrates e^{ax} against
|
||
|
|
// them. All are exact calculus, evaluated independently of the code
|
||
|
|
// under test.
|
||
|
|
func intFilon1(a, b, k float64) (c, s float64) {
|
||
|
|
return (math.Sin(k*b) - math.Sin(k*a)) / k,
|
||
|
|
(math.Cos(k*a) - math.Cos(k*b)) / k
|
||
|
|
}
|
||
|
|
|
||
|
|
func intFilonX(a, b, k float64) (c, s float64) {
|
||
|
|
cb, sb := math.Cos(k*b), math.Sin(k*b)
|
||
|
|
ca, sa := math.Cos(k*a), math.Sin(k*a)
|
||
|
|
c = (cb+k*b*sb)/k/k - (ca+k*a*sa)/k/k
|
||
|
|
s = (sb-k*b*cb)/k/k - (sa-k*a*ca)/k/k
|
||
|
|
return c, s
|
||
|
|
}
|
||
|
|
|
||
|
|
func intFilonExp(a, b, amp, k float64) (c, s float64) {
|
||
|
|
cb, sb := math.Cos(k*b), math.Sin(k*b)
|
||
|
|
ca, sa := math.Cos(k*a), math.Sin(k*a)
|
||
|
|
eb, ea := math.Exp(amp*b), math.Exp(amp*a)
|
||
|
|
c = eb*(amp*cb+k*sb)/(amp*amp+k*k) - ea*(amp*ca+k*sa)/(amp*amp+k*k)
|
||
|
|
s = eb*(amp*sb-k*cb)/(amp*amp+k*k) - ea*(amp*sa-k*ca)/(amp*amp+k*k)
|
||
|
|
return c, s
|
||
|
|
}
|
||
|
|
|
||
|
|
func wantFilon(t *testing.T, label string, gotC, gotS, wantC, wantS, tol float64) {
|
||
|
|
t.Helper()
|
||
|
|
if d := math.Abs(gotC - wantC); d > tol*math.Max(1, math.Abs(wantC)) {
|
||
|
|
t.Fatalf("%s: cos part = %.17g, want %.17g (absolute %.3g)", label, gotC, wantC, d)
|
||
|
|
}
|
||
|
|
if d := math.Abs(gotS - wantS); d > tol*math.Max(1, math.Abs(wantS)) {
|
||
|
|
t.Fatalf("%s: sin part = %.17g, want %.17g (absolute %.3g)", label, gotS, wantS, d)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestIntegrateFilonExactAmplitudes pins the exactness the method
|
||
|
|
// promises: unit and linear amplitudes are polynomials below the
|
||
|
|
// default degree, so every frequency from one to a thousand must land
|
||
|
|
// on the closed form at the rounding floor, whatever the carrier does
|
||
|
|
// between the samples.
|
||
|
|
func TestIntegrateFilonExactAmplitudes(t *testing.T) {
|
||
|
|
one := func(float64) (float64, error) { return 1, nil }
|
||
|
|
identity := func(x float64) (float64, error) { return x, nil }
|
||
|
|
for _, c := range []struct {
|
||
|
|
label string
|
||
|
|
a, b float64
|
||
|
|
k float64
|
||
|
|
f func(float64) (float64, error)
|
||
|
|
ref func(a, b, k float64) (c, s float64)
|
||
|
|
}{
|
||
|
|
{"unit on [0, π]", 0, math.Pi, 1, one, intFilon1},
|
||
|
|
{"unit on [2, 7]", 2, 7, 100, one, intFilon1},
|
||
|
|
{"unit on [0, 1]", 0, 1, 1000, one, intFilon1},
|
||
|
|
{"x on [0, π]", 0, math.Pi, 1, identity, intFilonX},
|
||
|
|
{"x on [2, 7]", 2, 7, 500, identity, intFilonX},
|
||
|
|
} {
|
||
|
|
gotC, gotS, err := IntegrateFilon(c.f, c.a, c.b, c.k, FilonOptions{})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("%s: %v", c.label, err)
|
||
|
|
}
|
||
|
|
wantC, wantS := c.ref(c.a, c.b, c.k)
|
||
|
|
wantFilon(t, c.label, gotC, gotS, wantC, wantS, 1e-12)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestIntegrateFilonZeroFrequency pins the degeneration at k = 0: the
|
||
|
|
// sine part is exactly zero and the cosine part is the plain integral
|
||
|
|
// of the amplitude.
|
||
|
|
func TestIntegrateFilonZeroFrequency(t *testing.T) {
|
||
|
|
f := func(x float64) (float64, error) { return x * x, nil }
|
||
|
|
gotC, gotS, err := IntegrateFilon(f, 0, 3, 0, FilonOptions{})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("IntegrateFilon: %v", err)
|
||
|
|
}
|
||
|
|
if gotS != 0 {
|
||
|
|
t.Fatalf("the sine part at k = 0 is %g, want 0", gotS)
|
||
|
|
}
|
||
|
|
if d := math.Abs(gotC - 9); d > 1e-12 {
|
||
|
|
t.Fatalf("the cosine part at k = 0 is %.17g, want 9", gotC)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestIntegrateFilonExponential holds a non-polynomial amplitude
|
||
|
|
// against the exact antiderivative at a frequency whose carrier the
|
||
|
|
// automatic panel count must respect: two hundred and fifty
|
||
|
|
// oscillations over the interval, answered from a few thousand
|
||
|
|
// amplitude samples.
|
||
|
|
func TestIntegrateFilonExponential(t *testing.T) {
|
||
|
|
const amp = 0.5
|
||
|
|
f := func(x float64) (float64, error) { return math.Exp(amp * x), nil }
|
||
|
|
gotC, gotS, err := IntegrateFilon(f, 2, 7, 500, FilonOptions{})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("IntegrateFilon: %v", err)
|
||
|
|
}
|
||
|
|
wantC, wantS := intFilonExp(2, 7, amp, 500)
|
||
|
|
wantFilon(t, "exp amplitude", gotC, gotS, wantC, wantS, 1e-11)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestIntegrateFilonOrientation pins the reversed interval and the
|
||
|
|
// empty one: reversing negates both parts and an empty interval
|
||
|
|
// integrates to nothing.
|
||
|
|
func TestIntegrateFilonOrientation(t *testing.T) {
|
||
|
|
f := func(x float64) (float64, error) { return math.Exp(0.2 * x), nil }
|
||
|
|
fc, fs, err := IntegrateFilon(f, 0, 3, 40, FilonOptions{})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
rc, rs, err := IntegrateFilon(f, 3, 0, 40, FilonOptions{})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if rc != -fc || rs != -fs {
|
||
|
|
t.Fatalf("the reversed interval gave (%.17g, %.17g), want the negation of (%.17g, %.17g)", rc, rs, fc, fs)
|
||
|
|
}
|
||
|
|
if ec, es, err := IntegrateFilon(f, 2, 2, 40, FilonOptions{}); err != nil || ec != 0 || es != 0 {
|
||
|
|
t.Fatalf("the empty interval gave (%g, %g, %v)", ec, es, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestIntegrateFilonDeterministic redraws one integral and requires
|
||
|
|
// the same bits, the contract every entry point here carries.
|
||
|
|
func TestIntegrateFilonDeterministic(t *testing.T) {
|
||
|
|
f := func(x float64) (float64, error) { return math.Exp(0.1 * x), nil }
|
||
|
|
one := func() (float64, float64) {
|
||
|
|
c, s, err := IntegrateFilon(f, 0, 5, 300, FilonOptions{})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
return c, s
|
||
|
|
}
|
||
|
|
c1, s1 := one()
|
||
|
|
c2, s2 := one()
|
||
|
|
if c1 != c2 || s1 != s2 {
|
||
|
|
t.Fatalf("the same call moved: (%.17g, %.17g) against (%.17g, %.17g)", c1, s1, c2, s2)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestIntegrateFilonErrors pins the contract: NaN bounds or frequency,
|
||
|
|
// a node count out of range, a forced panel count whose panels carry
|
||
|
|
// more carrier than the weights can be built within, and a failing or
|
||
|
|
// non-finite amplitude all surface as errors naming themselves.
|
||
|
|
func TestIntegrateFilonErrors(t *testing.T) {
|
||
|
|
f := func(x float64) (float64, error) { return 1, nil }
|
||
|
|
if _, _, err := IntegrateFilon(f, math.NaN(), 1, 10, FilonOptions{}); err == nil {
|
||
|
|
t.Fatal("a NaN bound: want an error")
|
||
|
|
}
|
||
|
|
if _, _, err := IntegrateFilon(f, 0, 1, math.NaN(), FilonOptions{}); err == nil {
|
||
|
|
t.Fatal("a NaN frequency: want an error")
|
||
|
|
}
|
||
|
|
if _, _, err := IntegrateFilon(f, 0, 1, math.Inf(1), FilonOptions{}); err == nil {
|
||
|
|
t.Fatal("an infinite frequency: want an error")
|
||
|
|
}
|
||
|
|
if _, _, err := IntegrateFilon(f, 0, 1, 10, FilonOptions{Nodes: 1}); err == nil {
|
||
|
|
t.Fatal("one node: want an error")
|
||
|
|
}
|
||
|
|
if _, _, err := IntegrateFilon(f, 0, 1, 10, FilonOptions{Nodes: 33}); err == nil {
|
||
|
|
t.Fatal("33 nodes: want an error")
|
||
|
|
}
|
||
|
|
if _, _, err := IntegrateFilon(f, 0, 1, 1e6, FilonOptions{Panels: 2}); err == nil {
|
||
|
|
t.Fatal("two panels under a carrier of 1e6: want an error")
|
||
|
|
}
|
||
|
|
boom := func(float64) (float64, error) { return 0, base.Errf("amplitude failed") }
|
||
|
|
if _, _, err := IntegrateFilon(boom, 0, 1, 10, FilonOptions{}); err == nil {
|
||
|
|
t.Fatal("a failing amplitude: want the error to propagate")
|
||
|
|
}
|
||
|
|
bad := func(x float64) (float64, error) {
|
||
|
|
if x > 0.5 {
|
||
|
|
return math.NaN(), nil
|
||
|
|
}
|
||
|
|
return 1, nil
|
||
|
|
}
|
||
|
|
if _, _, err := IntegrateFilon(bad, 0, 1, 10, FilonOptions{}); err == nil {
|
||
|
|
t.Fatal("a non-finite amplitude value: want an error")
|
||
|
|
}
|
||
|
|
}
|