feat: initial release
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integrate
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/big"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// IntegrateFilon against an external exact reference: the antiderivative
|
||||
//
|
||||
// ∫ p(x)·e^{ikx} dx = e^{ikx}·Σ_{j≥0} (−1)^j p^{(j)}(x)/(ik)^{j+1},
|
||||
//
|
||||
// summed in math/big at a working size far past the float64 grid, with
|
||||
// π from Machin's formula and the endpoint phases reduced mod 2π before
|
||||
// the Taylor run. The reference holds for every frequency tried here,
|
||||
// so a phase defect of the scheme itself shows against it.
|
||||
|
||||
const filonRefPrec = 512
|
||||
|
||||
func fb(x float64) *big.Float {
|
||||
return new(big.Float).SetPrec(filonRefPrec).SetFloat64(x)
|
||||
}
|
||||
|
||||
func fbInt(n int64) *big.Float {
|
||||
return new(big.Float).SetPrec(filonRefPrec).SetInt64(n)
|
||||
}
|
||||
|
||||
func fbPi() *big.Float {
|
||||
// π = 16·atan(1/5) − 4·atan(1/239).
|
||||
atan := func(t *big.Float) *big.Float {
|
||||
power := new(big.Float).SetPrec(filonRefPrec).Set(t)
|
||||
sum := fb(0)
|
||||
for k := int64(1); ; k += 2 {
|
||||
term := new(big.Float).SetPrec(filonRefPrec).Quo(power, fbInt(k))
|
||||
if (k/2)%2 == 1 {
|
||||
term.Neg(term)
|
||||
}
|
||||
sum.Add(sum, term)
|
||||
power.Mul(power, t)
|
||||
power.Mul(power, t)
|
||||
if term.MantExp(nil) < -int(filonRefPrec)-10 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return sum
|
||||
}
|
||||
// 1/5 must reach atan as the exact quotient: the float64 literal
|
||||
// 0.2 carries a 1e-17 argument error that Machin's formula
|
||||
// amplifies sixteenfold into π itself.
|
||||
fifth := new(big.Float).SetPrec(filonRefPrec).Quo(fb(1), fbInt(5))
|
||||
two39 := new(big.Float).SetPrec(filonRefPrec).Quo(fb(1), fbInt(239))
|
||||
sixteen := new(big.Float).SetPrec(filonRefPrec).Mul(fbInt(16), atan(fifth))
|
||||
four := new(big.Float).SetPrec(filonRefPrec).Mul(fbInt(4), atan(two39))
|
||||
return sixteen.Sub(sixteen, four)
|
||||
}
|
||||
|
||||
var (
|
||||
filonTwoPi = new(big.Float).SetPrec(filonRefPrec).Mul(fb(2), fbPi())
|
||||
filonPi = fbPi()
|
||||
)
|
||||
|
||||
// filonBigSinCos returns sin(x), cos(x) for the exact big.Float argument,
|
||||
// kept in extended precision: the endpoint products below multiply them
|
||||
// by antiderivative terms far larger than the integral itself, so a
|
||||
// float64 detour here would show up in the reference's own answer.
|
||||
func filonBigSinCos(x *big.Float) (s, c *big.Float) {
|
||||
n := new(big.Float).SetPrec(filonRefPrec).Quo(x, filonTwoPi)
|
||||
ni, _ := n.Int(nil)
|
||||
r := new(big.Float).SetPrec(filonRefPrec).Mul(new(big.Float).SetInt(ni), filonTwoPi)
|
||||
r.Sub(x, r)
|
||||
// The remainder sits within (−2π, 2π); one step puts it in (−π, π].
|
||||
halfPi := new(big.Float).SetPrec(filonRefPrec).Quo(filonPi, fb(2))
|
||||
if r.Cmp(halfPi) > 0 {
|
||||
r.Sub(r, filonTwoPi)
|
||||
} else if r.Cmp(new(big.Float).SetPrec(filonRefPrec).Neg(halfPi)) < 0 {
|
||||
r.Add(r, filonTwoPi)
|
||||
}
|
||||
// Taylor runs about the reduced argument; the zero remainder is the
|
||||
// exact answer both series converge to.
|
||||
if r.Sign() == 0 {
|
||||
return fb(0), fb(1)
|
||||
}
|
||||
r2 := new(big.Float).SetPrec(filonRefPrec).Mul(r, r)
|
||||
ts, tc := new(big.Float).SetPrec(filonRefPrec).Set(r), fb(1)
|
||||
sumS, sumC := new(big.Float).SetPrec(filonRefPrec).Set(r), fb(1)
|
||||
for j := int64(1); ; j++ {
|
||||
ts.Mul(ts, r2)
|
||||
ts.Quo(ts, fbInt((2*j)*(2*j+1)))
|
||||
ts.Neg(ts)
|
||||
sumS.Add(sumS, ts)
|
||||
tc.Mul(tc, r2)
|
||||
tc.Quo(tc, fbInt((2*j-1)*(2*j)))
|
||||
tc.Neg(tc)
|
||||
sumC.Add(sumC, tc)
|
||||
if ts.Sign() == 0 || ts.MantExp(nil) < -int(filonRefPrec)-10 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return sumS, sumC
|
||||
}
|
||||
|
||||
// poly is a real polynomial, coefficients ascending.
|
||||
type poly []float64
|
||||
|
||||
func (p poly) evalBig(x *big.Float) *big.Float {
|
||||
acc := fb(0)
|
||||
for _, v := range slices.Backward(p) {
|
||||
acc.Mul(acc, x)
|
||||
acc.Add(acc, fb(v))
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
||||
// formalDeriv differentiates the coefficient list.
|
||||
func (p poly) formalDeriv() poly {
|
||||
if len(p) <= 1 {
|
||||
return poly{0}
|
||||
}
|
||||
d := make(poly, len(p)-1)
|
||||
for i := 1; i < len(p); i++ {
|
||||
d[i-1] = float64(i) * p[i]
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// filonPolyRef evaluates ∫ₐ^b p(x)·cos(kx) dx and the sine part against
|
||||
// the antiderivative above, in extended precision.
|
||||
func filonPolyRef(p poly, a, b, k float64) (c, s float64) {
|
||||
endpoint := func(x float64) (re, im *big.Float) {
|
||||
// Q(x) = Σ (−1)^j p^{(j)}(x)/(ik)^{j+1}, split by the cycle of
|
||||
// i^{−(j+1)}: −i, −1, i, 1.
|
||||
qre, qim := fb(0), fb(0)
|
||||
sign := fb(1)
|
||||
kpow := fb(1) // k^(j+1), built by repeated multiplication
|
||||
px := fb(x)
|
||||
pp := p
|
||||
value := pp.evalBig(px)
|
||||
for j := range p {
|
||||
kpow.Mul(kpow, fb(k))
|
||||
scale := new(big.Float).SetPrec(filonRefPrec).Quo(sign, kpow)
|
||||
switch (j + 1) % 4 {
|
||||
case 1: // −i
|
||||
qim.Sub(qim, scale.Mul(scale, value))
|
||||
case 2: // −1
|
||||
qre.Sub(qre, scale.Mul(scale, value))
|
||||
case 3: // i
|
||||
qim.Add(qim, scale.Mul(scale, value))
|
||||
default: // 1
|
||||
qre.Add(qre, scale.Mul(scale, value))
|
||||
}
|
||||
sign.Neg(sign)
|
||||
// p^{(j+1)} for the next term.
|
||||
pp = pp.formalDeriv()
|
||||
value = pp.evalBig(px)
|
||||
}
|
||||
sb, cb := filonBigSinCos(fb(k * x))
|
||||
ere := new(big.Float).SetPrec(filonRefPrec).Mul(cb, qre)
|
||||
ere.Sub(ere, new(big.Float).SetPrec(filonRefPrec).Mul(sb, qim))
|
||||
eim := new(big.Float).SetPrec(filonRefPrec).Mul(sb, qre)
|
||||
eim.Add(eim, new(big.Float).SetPrec(filonRefPrec).Mul(cb, qim))
|
||||
return ere, eim
|
||||
}
|
||||
reB, imB := endpoint(b)
|
||||
reA, imA := endpoint(a)
|
||||
cv, _ := reB.Sub(reB, reA).Float64()
|
||||
sv, _ := imB.Sub(imB, imA).Float64()
|
||||
return cv, sv
|
||||
}
|
||||
|
||||
func filonRelErr(got, want float64) float64 {
|
||||
if want == 0 {
|
||||
return math.Abs(got)
|
||||
}
|
||||
return math.Abs(got-want) / math.Abs(want)
|
||||
}
|
||||
|
||||
// ampScale bounds |p| over [a, b] by the sum of the coefficients'
|
||||
// magnitudes lifted to the interval's ends, the scale the absolute
|
||||
// tolerance is measured against: at high frequency the integral itself
|
||||
// can cancel to nearly nothing and a relative metric would chase noise.
|
||||
func ampScale(p poly, a, b float64) float64 {
|
||||
mag := math.Max(1, math.Max(math.Abs(a), math.Abs(b)))
|
||||
s := 0.0
|
||||
pow := 1.0
|
||||
for _, c := range p {
|
||||
s += math.Abs(c) * pow
|
||||
pow *= mag
|
||||
}
|
||||
return math.Abs(b-a) * s
|
||||
}
|
||||
|
||||
// TestIntegrateFilonPolynomialMoments holds the scheme against the exact
|
||||
// antiderivative across amplitudes of every degree the default node
|
||||
// count interpolates exactly, intervals with both orientations' worth of
|
||||
// geometry, and frequencies from the settled to the far oscillatory.
|
||||
func TestIntegrateFilonPolynomialMoments(t *testing.T) {
|
||||
amplitudes := map[string]poly{
|
||||
"1": {1},
|
||||
"2 − 3x + x²": {2, -3, 1},
|
||||
"1 + 0.5x³": {1, 0, 0, 0.5},
|
||||
"x − 2x⁴ + 4x⁷": {0, 1, 0, 0, -2, 0, 0, 4},
|
||||
// Degree 15, the exact interpolation degree of the default 16
|
||||
// nodes.
|
||||
"degree 15": {1, -1, 0.5, 0.25, -0.125, 0.0625, 0.5, -0.5, 0.25, -0.25, 0.125, -0.125, 0.0625, -0.0625, 0.5, -0.25},
|
||||
}
|
||||
worst := 0.0
|
||||
for _, k := range []float64{1, 10, 100, 1000, 10000} {
|
||||
for label, p := range amplitudes {
|
||||
for _, c := range [][2]float64{{0, 1}, {2, 7}, {-1, 3}} {
|
||||
a, b := c[0], c[1]
|
||||
gotC, gotS, err := IntegrateFilon(plainPoly(p), a, b, k, FilonOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("%s on [%g,%g] k=%g: %v", label, a, b, k, err)
|
||||
}
|
||||
wantC, wantS := filonPolyRef(p, a, b, k)
|
||||
scale := ampScale(p, a, b)
|
||||
dC := math.Abs(gotC-wantC) / scale
|
||||
dS := math.Abs(gotS-wantS) / scale
|
||||
worst = math.Max(worst, math.Max(dC, dS))
|
||||
if dC > 1e-13 {
|
||||
t.Fatalf("%s on [%g,%g] k=%g: cos part %.17g against exact %.17g (scaled %.3g)",
|
||||
label, a, b, k, gotC, wantC, dC)
|
||||
}
|
||||
if dS > 1e-13 {
|
||||
t.Fatalf("%s on [%g,%g] k=%g: sin part %.17g against exact %.17g (scaled %.3g)",
|
||||
label, a, b, k, gotS, wantS, dS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Logf("worst scaled moment error across the sweep: %.3e", worst)
|
||||
}
|
||||
|
||||
// plainPoly wraps a polynomial as the amplitude IntegrateFilon samples.
|
||||
func plainPoly(p poly) func(float64) (float64, error) {
|
||||
return func(x float64) (float64, error) {
|
||||
acc := 0.0
|
||||
for _, v := range slices.Backward(p) {
|
||||
acc = acc*x + v
|
||||
}
|
||||
return acc, nil
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrateFilonLargeFrequency pins the phase at frequencies where a
|
||||
// float64 antiderivative would stop being a reference: the extended
|
||||
// precision one keeps counting. The bound is absolute against the
|
||||
// amplitude scale, because the integral itself shrinks like 1/k.
|
||||
func TestIntegrateFilonLargeFrequency(t *testing.T) {
|
||||
amplitudes := map[string]poly{
|
||||
"2 − 3x + x²": {2, -3, 1},
|
||||
"1 + 0.5x³": {1, 0, 0, 0.5},
|
||||
"x − 2x⁴ + 4x⁷": {0, 1, 0, 0, -2, 0, 0, 4},
|
||||
}
|
||||
worst := 0.0
|
||||
for _, k := range []float64{1e5, 1e6} {
|
||||
for label, p := range amplitudes {
|
||||
gotC, gotS, err := IntegrateFilon(plainPoly(p), 0, 1, k, FilonOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("%s at k=%g: %v", label, k, err)
|
||||
}
|
||||
wantC, wantS := filonPolyRef(p, 0, 1, k)
|
||||
scale := ampScale(p, 0, 1)
|
||||
dC, dS := math.Abs(gotC-wantC)/scale, math.Abs(gotS-wantS)/scale
|
||||
t.Logf("k=%g %s: cos %.3e, sin %.3e (scaled absolute)", k, label, dC, dS)
|
||||
worst = math.Max(worst, math.Max(dC, dS))
|
||||
}
|
||||
}
|
||||
if worst > 1e-11 {
|
||||
t.Fatalf("the worst large-frequency scaled error %.3e is past the phase budget", worst)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrateFilonBeatsPlainQuad measures the scheme's reason to exist:
|
||||
// at equal evaluation budgets the plain adaptive rule must resolve the
|
||||
// carrier while Filon tracks the amplitude, and the gap has to be worth
|
||||
// the second entry point.
|
||||
func TestIntegrateFilonBeatsPlainQuad(t *testing.T) {
|
||||
p := poly{0, 1, 0, 0, -2, 0, 0, 4}
|
||||
for _, k := range []float64{1000, 10000} {
|
||||
wantC, _ := filonPolyRef(p, 0, 1, k)
|
||||
// Filon's budget: automatic panels times the default node count.
|
||||
panels := int(math.Ceil(k / (2 * 16)))
|
||||
budget := panels * 16
|
||||
// The plain rule pays 31 evaluations per subinterval (a 21-point
|
||||
// rule and a 10-point rule on every leaf).
|
||||
leaves := budget / 31
|
||||
counts := 0
|
||||
amp := func(x float64) (float64, error) {
|
||||
counts++
|
||||
v, err := plainPoly(p)(x)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return v * math.Cos(k*x), nil
|
||||
}
|
||||
got, _, err := IntegrateFunction(amp, 0, 1, QuadratureOptions{MaxIntervals: leaves})
|
||||
quadErr := math.Inf(1)
|
||||
if err != nil {
|
||||
t.Logf("k=%g: the plain rule failed within %d leaves (%d evaluations): %v", k, leaves, counts, err)
|
||||
} else {
|
||||
quadErr = filonRelErr(got, wantC)
|
||||
t.Logf("k=%g: plain quad %.3e (%d evaluations), Filon on the same budget below", k, quadErr, counts)
|
||||
}
|
||||
fCounts := 0
|
||||
f := func(x float64) (float64, error) {
|
||||
fCounts++
|
||||
return plainPoly(p)(x)
|
||||
}
|
||||
gotC, _, err := IntegrateFilon(f, 0, 1, k, FilonOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fErr := filonRelErr(gotC, wantC)
|
||||
t.Logf("k=%g: Filon %.3e from %d evaluations", k, fErr, fCounts)
|
||||
if fCounts > budget {
|
||||
t.Fatalf("Filon spent %d evaluations past its own budget %d", fCounts, budget)
|
||||
}
|
||||
if fErr >= quadErr {
|
||||
t.Fatalf("k=%g: Filon's error %.3e fails to beat the plain rule's %.3e on the same budget", k, fErr, quadErr)
|
||||
}
|
||||
if fErr*100 > quadErr {
|
||||
t.Fatalf("k=%g: Filon's error %.3e is within two orders of the plain rule's %.3e", k, fErr, quadErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user