146 lines
3.9 KiB
Go
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")
|
|
}
|
|
}
|