Files
tensor/integrate/pde_test.go
T
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

146 lines
3.9 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/core"
)
// TestHeatEigenmodeDecay pins the analytic solution: the first sine
// eigenmode decays as exp(−κ·π²·t/L²).
func TestHeatEigenmodeDecay(t *testing.T) {
const (
n = 49
L = 1.0
kappa = 0.1
)
dx := L / float64(n+1)
u0 := make([]float64, n)
for i := range n {
u0[i] = math.Sin(math.Pi * float64(i+1) * dx / L)
}
u0Arr, _ := core.FromFloats(u0, n)
const tFinal = 1.0
states, err := IntegrateHeat1D(u0Arr, kappa, dx, tFinal, 0.002, 3, 0, 0)
if err != nil {
t.Fatalf("IntegrateHeat1D: %v", err)
}
decay := math.Exp(-kappa * math.Pi * math.Pi * tFinal / (L * L))
final := states.Shape()[0]*n - n
for i := range n {
got := states.FloatAt(final + i)
want := u0[i] * decay
if math.Abs(got-want) > 5e-4*decay {
t.Fatalf("u[%d] = %.8f, eigenmode says %.8f", i, got, want)
}
}
}
// TestHeatConservesConstantWithZeroBounds pins the fixed-point: a
// constant field with equal Dirichlet bounds never moves.
func TestHeatConservesConstantWithZeroBounds(t *testing.T) {
const n = 20
u0 := make([]float64, n)
for i := range n {
u0[i] = 2.5
}
u0Arr, _ := core.FromFloats(u0, n)
states, err := IntegrateHeat1D(u0Arr, 1.0, 0.1, 1.0, 0.05, 5, 2.5, 2.5)
if err != nil {
t.Fatalf("IntegrateHeat1D: %v", err)
}
last := (states.Shape()[0] - 1) * n
for i := range n {
if math.Abs(states.FloatAt(last+i)-2.5) > 1e-12 {
t.Fatalf("constant field drifted: u[%d] = %.12f", i, states.FloatAt(last+i))
}
}
}
// TestWaveStandingFrequency pins a standing wave: the fundamental mode
// u(x, t) = sin(πx)·cos(πct) must return to (minus) itself after half
// a period.
func TestWaveStandingFrequency(t *testing.T) {
const (
n = 99
L = 1.0
c = 1.0
)
dx := L / float64(n+1)
u0 := make([]float64, n)
for i := range n {
u0[i] = math.Sin(math.Pi * float64(i+1) * dx / L)
}
u0Arr, _ := core.FromFloats(u0, n)
v0, _ := core.FromFloats(make([]float64, n), n)
// Half period of the fundamental: T1/2 = L/c.
states, err := IntegrateWave1D(u0Arr, v0, c, dx, 1.0, 0.001, 2)
if err != nil {
t.Fatalf("IntegrateWave1D: %v", err)
}
last := (states.Shape()[0] - 1) * n
for i := range n {
got := states.FloatAt(last + i)
want := -u0[i]
if math.Abs(got-want) > 2e-3 {
t.Fatalf("standing wave off after half period: u[%d] = %.6f, want %.6f", i, got, want)
}
}
}
// TestWaveEnergyBand pins Verlet's bounded energy over many periods.
func TestWaveEnergyBand(t *testing.T) {
const (
n = 79
L = 1.0
c = 1.0
)
dx := L / float64(n+1)
u0 := make([]float64, n)
for i := range n {
u0[i] = math.Sin(math.Pi*float64(i+1)*dx/L) + 0.3*math.Sin(3*math.Pi*float64(i+1)*dx/L)
}
u0Arr, _ := core.FromFloats(u0, n)
v0, _ := core.FromFloats(make([]float64, n), n)
states, err := IntegrateWave1D(u0Arr, v0, c, dx, 10.0, 0.002, 11)
if err != nil {
t.Fatalf("IntegrateWave1D: %v", err)
}
energy := func(row int) float64 {
e := 0.0
for i := range n {
e += states.FloatAt(row*n+i) * states.FloatAt(row*n+i)
}
return e
}
e0 := energy(0)
for row := 1; row < 11; row++ {
e := energy(row)
if math.Abs(e-e0) > 1e-3*e0 {
t.Fatalf("energy drifted: row %d has %.8f vs %.8f", row, e, e0)
}
}
}
// TestPDEErrors pins the input gates.
func TestPDEErrors(t *testing.T) {
u, _ := core.FromFloats([]float64{1, 2, 3}, 3)
if _, err := IntegrateHeat1D(u, -1, 0.1, 1, 0.01, 2, 0, 0); err == nil {
t.Error("negative diffusivity accepted")
}
if _, err := IntegrateHeat1D(u, 1, 0.1, 1, 0.01, 1, 0, 0); err == nil {
t.Error("one sample accepted")
}
v, _ := core.FromFloats([]float64{1, 2}, 2)
if _, err := IntegrateWave1D(u, v, 1, 0.1, 1, 0.01, 2); err == nil {
t.Error("velocity shape mismatch accepted")
}
if _, err := IntegrateWave1D(u, u, 1, 0.1, 1, 0.2, 2); err == nil {
t.Error("CFL violation accepted")
}
}