Files
tensor/signal/poissondirichlet_test.go
petrbalvin af4ee19703
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s
feat: initial release
Assisted-by: GLM 5.3 Flash
2026-09-03 10:00:00 +02:00

131 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package signal
import (
"math"
"testing"
"sourcedock.dev/petrbalvin/tensor/internal/core"
)
// poissonManufactured builds f = −Δu for u = sin(πx)·sin(πy) on the
// unit square over an (n × n) grid including the boundary, so the
// exact solution is known everywhere.
func poissonManufactured(t *testing.T, n int) (f, exact *core.Array) {
t.Helper()
flatF := make([]float64, n*n)
flatU := make([]float64, n*n)
for r := range n {
for c := range n {
x := float64(c) / float64(n-1)
y := float64(r) / float64(n-1)
i := r*n + c
flatU[i] = math.Sin(math.Pi*x) * math.Sin(math.Pi*y)
flatF[i] = 2 * math.Pi * math.Pi * flatU[i]
}
}
f = mustFloats(t, flatF, n, n)
exact = mustFloats(t, flatU, n, n)
return f, exact
}
// TestSolvePoissonDirichletManufactured checks the sine-solve against
// the manufactured solution: second-order convergence, with the
// coarse grid already inside 5e-3.
func TestSolvePoissonDirichletManufactured(t *testing.T) {
for _, n := range []int{9, 17, 33} {
f, exact := poissonManufactured(t, n)
u, err := SolvePoissonDirichlet(f, 1, 1)
if err != nil {
t.Fatalf("SolvePoissonDirichlet(%d): %v", n, err)
}
worst := 0.0
for i := range exact.Len() {
if e := math.Abs(u.FloatAt(i) - exact.FloatAt(i)); e > worst {
worst = e
}
}
h := 1 / float64(n-1)
if worst > 3*h*h {
t.Fatalf("grid %d: worst error %.3g above the O(h²) budget %.3g", n, worst, 3*h*h)
}
// The boundary is exactly zero by construction.
for c := range n {
if u.FloatAt(c) != 0 || u.FloatAt((n-1)*n+c) != 0 {
t.Fatalf("grid %d: boundary came back nonzero", n)
}
}
}
}
// TestSolvePoissonNeumannManufactured checks the cosine solve with a
// zero-mean source whose solution is u = cos(πx)·cos(πy), the
// derivative-free data the Neumann solve exists for.
func TestSolvePoissonNeumannManufactured(t *testing.T) {
const n = 17
flatF := make([]float64, n*n)
flatU := make([]float64, n*n)
for r := range n {
for c := range n {
x := float64(c) / float64(n-1)
y := float64(r) / float64(n-1)
i := r*n + c
flatU[i] = math.Cos(math.Pi*x) * math.Cos(math.Pi*y)
flatF[i] = 2 * math.Pi * math.Pi * flatU[i]
}
}
// Zero-mean shift: subtract the mean, which also shifts u by a
// constant the Neumann problem cannot see.
meanF := 0.0
for _, v := range flatF {
meanF += v
}
meanF /= float64(n * n)
for i := range flatF {
flatF[i] -= meanF
}
f := mustFloats(t, flatF, n, n)
exact := mustFloats(t, flatU, n, n)
u, err := SolvePoissonNeumann(f, 1, 1)
if err != nil {
t.Fatalf("SolvePoissonNeumann: %v", err)
}
// Compare up to the free constant: recentre both to zero mean.
got := 0.0
for i := range u.Len() {
got += u.FloatAt(i)
}
got /= float64(u.Len())
worst := 0.0
for i := range u.Len() {
if e := math.Abs(u.FloatAt(i) - got - exact.FloatAt(i)); e > worst {
worst = e
}
}
h := 1 / float64(n-1)
if worst > 3*h*h {
t.Fatalf("worst error %.3g above the O(h²) budget %.3g", worst, 3*h*h)
}
}
// TestSolvePoissonNeumannMeanRefusal checks the compatibility
// condition: a nonzero-mean source has no Neumann solution.
func TestSolvePoissonNeumannMeanRefusal(t *testing.T) {
f := mustFloats(t, []float64{
1, 1, 1,
1, 1, 1,
1, 1, 1,
}, 3, 3)
if _, err := SolvePoissonNeumann(f, 1, 1); err == nil {
t.Fatal("nonzero-mean source accepted")
}
if _, err := SolvePoissonDirichlet(mustFloats(t, []float64{1, 1}, 1, 2), 1, 1); err == nil {
t.Fatal("grid under 3×3 accepted")
}
if _, err := SolvePoissonDirichlet(f, 0, 1); err == nil {
t.Fatal("non-positive length accepted")
}
}