144 lines
5.4 KiB
Go
144 lines
5.4 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
|
|
// SPDX-License-Identifier: MIT
|
|||
|
|
|
|||
|
|
package signal
|
|||
|
|
|
|||
|
|
import "sourcedock.dev/petrbalvin/tensor/internal/core"
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"math"
|
|||
|
|
"testing"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// poissonGrid samples a function of (x, y) on the periodic square
|
|||
|
|
// [0, lx] × [0, ly] as a (rows × cols) array, matching the sampling
|
|||
|
|
// SolvePoissonPeriodic documents.
|
|||
|
|
func poissonGrid(t *testing.T, fn func(x, y float64) float64, rows, cols int, lx, ly float64) *core.Array {
|
|||
|
|
t.Helper()
|
|||
|
|
vals := make([]float64, rows*cols)
|
|||
|
|
for r := range rows {
|
|||
|
|
for c := range cols {
|
|||
|
|
vals[r*cols+c] = fn(float64(c)*lx/float64(cols), float64(r)*ly/float64(rows))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return mustFloats(t, vals, rows, cols)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// poissonError returns the largest absolute difference between the
|
|||
|
|
// solution and an exact function of (x, y) on the same grid.
|
|||
|
|
func poissonError(u *core.Array, want func(x, y float64) float64, rows, cols int, lx, ly float64) float64 {
|
|||
|
|
worst := 0.0
|
|||
|
|
for r := range rows {
|
|||
|
|
for c := range cols {
|
|||
|
|
d := math.Abs(u.FloatAt(r*cols+c) - want(float64(c)*lx/float64(cols), float64(r)*ly/float64(rows)))
|
|||
|
|
worst = math.Max(worst, d)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return worst
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestSolvePoissonPeriodicModes checks the plane-wave answers the
|
|||
|
|
// diagonalisation gets exactly: for u = sin x·sin y the Laplacian
|
|||
|
|
// gives −2u, and for u = sin 3x·cos 2y it gives −13u. Both land on
|
|||
|
|
// round-off.
|
|||
|
|
func TestSolvePoissonPeriodicModes(t *testing.T) {
|
|||
|
|
const lx, ly = 2 * math.Pi, 2 * math.Pi
|
|||
|
|
cases := []struct {
|
|||
|
|
f, u func(x, y float64) float64
|
|||
|
|
}{
|
|||
|
|
{
|
|||
|
|
f: func(x, y float64) float64 { return 2 * math.Sin(x) * math.Sin(y) },
|
|||
|
|
u: func(x, y float64) float64 { return math.Sin(x) * math.Sin(y) },
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
f: func(x, y float64) float64 { return 13 * math.Sin(3*x) * math.Cos(2*y) },
|
|||
|
|
u: func(x, y float64) float64 { return math.Sin(3*x) * math.Cos(2*y) },
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
for k, tc := range cases {
|
|||
|
|
grid := poissonGrid(t, tc.f, 16, 16, lx, ly)
|
|||
|
|
u, err := SolvePoissonPeriodic(grid, lx, ly)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("case %d: SolvePoissonPeriodic: %v", k, err)
|
|||
|
|
}
|
|||
|
|
if u.Dtype() == core.Complex {
|
|||
|
|
t.Fatalf("case %d: the solution must be real", k)
|
|||
|
|
}
|
|||
|
|
if got := poissonError(u, tc.u, 16, 16, lx, ly); got > 1e-10 {
|
|||
|
|
t.Fatalf("case %d: max error %.3g, want round-off", k, got)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestSolvePoissonPeriodicSpectral shows the convergence is spectral:
|
|||
|
|
// for u = sin x·e^{sin y}, whose Fourier coefficients decay faster
|
|||
|
|
// than any power, refining the grid drives the error from visible to
|
|||
|
|
// round-off in one refinement.
|
|||
|
|
func TestSolvePoissonPeriodicSpectral(t *testing.T) {
|
|||
|
|
const lx, ly = 2 * math.Pi, 2 * math.Pi
|
|||
|
|
f := func(x, y float64) float64 {
|
|||
|
|
return math.Sin(x) * math.Exp(math.Sin(y)) * (math.Sin(y)*math.Sin(y) + math.Sin(y))
|
|||
|
|
}
|
|||
|
|
u := func(x, y float64) float64 { return math.Sin(x) * math.Exp(math.Sin(y)) }
|
|||
|
|
errAt := func(n int) float64 {
|
|||
|
|
sol, err := SolvePoissonPeriodic(poissonGrid(t, f, n, n, lx, ly), lx, ly)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("SolvePoissonPeriodic(%d): %v", n, err)
|
|||
|
|
}
|
|||
|
|
return poissonError(sol, u, n, n, lx, ly)
|
|||
|
|
}
|
|||
|
|
coarse, fine, finer := errAt(16), errAt(32), errAt(64)
|
|||
|
|
if coarse < 1e-9 {
|
|||
|
|
t.Skipf("coarse grid already at round-off (%v)", coarse)
|
|||
|
|
}
|
|||
|
|
if fine >= coarse {
|
|||
|
|
t.Fatalf("refining the grid did not help: %.3g then %.3g", coarse, fine)
|
|||
|
|
}
|
|||
|
|
if fine > 1e-11 || finer > 1e-11 {
|
|||
|
|
t.Fatalf("spectral accuracy not reached: %.3g, %.3g", fine, finer)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestSolvePoissonPeriodicErrors pins the validation contract: the
|
|||
|
|
// zero-mean compatibility condition, the shape and size of the grid,
|
|||
|
|
// and positive side lengths.
|
|||
|
|
func TestSolvePoissonPeriodicErrors(t *testing.T) {
|
|||
|
|
const lx, ly = 2 * math.Pi, 2 * math.Pi
|
|||
|
|
constant := poissonGrid(t, func(x, y float64) float64 { return 1 }, 8, 8, lx, ly)
|
|||
|
|
if _, err := SolvePoissonPeriodic(constant, lx, ly); err == nil {
|
|||
|
|
t.Fatal("expected an error for a right-hand side with nonzero mean")
|
|||
|
|
}
|
|||
|
|
rank1 := mustFloats(t, []float64{0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8}, 8)
|
|||
|
|
if _, err := SolvePoissonPeriodic(rank1, lx, ly); err == nil {
|
|||
|
|
t.Fatal("expected an error for a rank-1 right-hand side")
|
|||
|
|
}
|
|||
|
|
small := poissonGrid(t, func(x, y float64) float64 { return x }, 1, 8, lx, ly)
|
|||
|
|
if _, err := SolvePoissonPeriodic(small, lx, ly); err == nil {
|
|||
|
|
t.Fatal("expected an error for a grid dimension below 2")
|
|||
|
|
}
|
|||
|
|
grid := poissonGrid(t, func(x, y float64) float64 { return math.Sin(x) }, 8, 8, lx, ly)
|
|||
|
|
if _, err := SolvePoissonPeriodic(grid, 0, ly); err == nil {
|
|||
|
|
t.Fatal("expected an error for a zero side length")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestSolvePoissonPeriodicDtypeContract pins the dtype contract: the
|
|||
|
|
// solver computes in float64 end to end, so a float32, int or complex
|
|||
|
|
// right-hand side is an error rather than a silently all-zero
|
|||
|
|
// solution (RawFloats is nil for those dtypes).
|
|||
|
|
func TestSolvePoissonPeriodicDtypeContract(t *testing.T) {
|
|||
|
|
const lx, ly = 2 * math.Pi, 2 * math.Pi
|
|||
|
|
f32, _ := core.FromFloat32s([]float32{1, 2, 3, 4, 5, 6, 7, 8, 8, 7, 6, 5, 4, 3, 2, 1}, 4, 4)
|
|||
|
|
if _, err := SolvePoissonPeriodic(f32, lx, ly); err == nil {
|
|||
|
|
t.Fatal("expected an error for a float32 right-hand side")
|
|||
|
|
}
|
|||
|
|
ints, _ := core.FromInts([]int64{1, 2, 3, 4, 5, 6, 7, 8, 8, 7, 6, 5, 4, 3, 2, 1}, 4, 4)
|
|||
|
|
if _, err := SolvePoissonPeriodic(ints, lx, ly); err == nil {
|
|||
|
|
t.Fatal("expected an error for an int right-hand side")
|
|||
|
|
}
|
|||
|
|
cplx, _ := core.FromComplexes(make([]complex128, 16), 4, 4)
|
|||
|
|
if _, err := SolvePoissonPeriodic(cplx, lx, ly); err == nil {
|
|||
|
|
t.Fatal("expected an error for a complex right-hand side")
|
|||
|
|
}
|
|||
|
|
}
|