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