feat: initial release
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user