feat: initial release
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package linalg
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sourcedock.dev/petrbalvin/tensor/internal/core"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// triDiagCOO builds a tridiagonal SparseCOO with the given diagonals.
|
||||
func triDiagCOO(t *testing.T, n int, lo, diag, up float64) *core.SparseCOO {
|
||||
t.Helper()
|
||||
idx := make([]int64, 0, 3*n)
|
||||
vals := make([]float64, 0, 3*n)
|
||||
add := func(r, c int, v float64) {
|
||||
idx = append(idx, int64(r), int64(c))
|
||||
vals = append(vals, v)
|
||||
}
|
||||
for i := range n {
|
||||
add(i, i, diag)
|
||||
if i+1 < n {
|
||||
add(i, i+1, up)
|
||||
add(i+1, i, lo)
|
||||
}
|
||||
}
|
||||
indices, err := core.FromInts(idx, len(vals), 2)
|
||||
if err != nil {
|
||||
t.Fatalf("FromInts: %v", err)
|
||||
}
|
||||
coo, err := core.NewSparseCOO(indices, floatsToArray(vals, []int{len(vals)}), []int{n, n})
|
||||
if err != nil {
|
||||
t.Fatalf("NewSparseCOO: %v", err)
|
||||
}
|
||||
return coo
|
||||
}
|
||||
|
||||
// TestSparseILUTridiagonalExact uses the fact that a tridiagonal
|
||||
// matrix creates no fill under LU: the ILU(0) factors must reproduce
|
||||
// the complete LU exactly, so applying them to a unit vector answers
|
||||
// what the dense Solve answers for the same right-hand side.
|
||||
func TestSparseILUTridiagonalExact(t *testing.T) {
|
||||
const n = 12
|
||||
coo := triDiagCOO(t, n, -1, 4, -2)
|
||||
ilu, err := NewSparseILU(coo)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSparseILU: %v", err)
|
||||
}
|
||||
denseVals := make([]float64, n*n)
|
||||
for i := range n {
|
||||
denseVals[i*n+i] = 4
|
||||
if i+1 < n {
|
||||
denseVals[i*n+i+1] = -2
|
||||
denseVals[(i+1)*n+i] = -1
|
||||
}
|
||||
}
|
||||
dense := mustFloats(t, denseVals, n, n)
|
||||
for j := range n {
|
||||
e := make([]float64, n)
|
||||
e[j] = 1
|
||||
got := ilu.Apply(e)
|
||||
ref, err := Solve(dense, mustFloats(t, e))
|
||||
if err != nil {
|
||||
t.Fatalf("Solve: %v", err)
|
||||
}
|
||||
for i := range n {
|
||||
if math.Abs(got[i]-ref.FloatAt(i)) > 1e-11 {
|
||||
t.Fatalf("column %d, row %d: %.14g, want %.14g", j, i, got[i], ref.FloatAt(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSparseILUWiderStencil checks the wider-than-tridiagonal case:
|
||||
// a five-point stencil with dropped fill cannot reproduce A exactly,
|
||||
// but the preconditioned residual of the factorisation must still be
|
||||
// far smaller than the identity preconditioner's.
|
||||
func TestSparseILUWiderStencil(t *testing.T) {
|
||||
const n = 20
|
||||
idx := make([]int64, 0, 5*n)
|
||||
vals := make([]float64, 0, 5*n)
|
||||
add := func(r, c int, v float64) {
|
||||
idx = append(idx, int64(r), int64(c))
|
||||
vals = append(vals, v)
|
||||
}
|
||||
for i := range n {
|
||||
add(i, i, 5)
|
||||
if i+1 < n {
|
||||
add(i, i+1, -2)
|
||||
add(i+1, i, -1)
|
||||
}
|
||||
if i+2 < n {
|
||||
add(i, i+2, 0.3)
|
||||
add(i+2, i, -0.2)
|
||||
}
|
||||
}
|
||||
indices, err := core.FromInts(idx, len(vals), 2)
|
||||
if err != nil {
|
||||
t.Fatalf("FromInts: %v", err)
|
||||
}
|
||||
coo, err := core.NewSparseCOO(indices, floatsToArray(vals, []int{len(vals)}), []int{n, n})
|
||||
if err != nil {
|
||||
t.Fatalf("NewSparseCOO: %v", err)
|
||||
}
|
||||
ilu, err := NewSparseILU(coo)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSparseILU: %v", err)
|
||||
}
|
||||
rhs := make([]float64, n)
|
||||
for i := range n {
|
||||
rhs[i] = math.Cos(float64(i))
|
||||
}
|
||||
x := ilu.Apply(rhs)
|
||||
// The residual ‖r − A·x‖ must sit well below ‖r‖: the incomplete
|
||||
// factorisation captures most of A.
|
||||
res := 0.0
|
||||
rhsNorm := 0.0
|
||||
for i := range n {
|
||||
s := rhs[i]
|
||||
rowSum := 0.0
|
||||
for k := range n {
|
||||
v := 0.0
|
||||
switch {
|
||||
case k == i:
|
||||
v = 5
|
||||
case k == i+1:
|
||||
v = -2
|
||||
case k == i-1:
|
||||
v = -1
|
||||
case k == i+2:
|
||||
v = 0.3
|
||||
case k == i-2:
|
||||
v = -0.2
|
||||
}
|
||||
rowSum += v * x[k]
|
||||
}
|
||||
d := s - rowSum
|
||||
res += d * d
|
||||
rhsNorm += s * s
|
||||
}
|
||||
if math.Sqrt(res) > 0.5*math.Sqrt(rhsNorm) {
|
||||
t.Fatalf("ILU residual %g too large against ‖r‖ %g", math.Sqrt(res), math.Sqrt(rhsNorm))
|
||||
}
|
||||
}
|
||||
|
||||
// TestSparseILUPreconditionedSteps checks the preconditioner's purpose:
|
||||
// on a convective 100×100 system the ILU-preconditioned BiCGSTAB must
|
||||
// reach the same tolerance as the Jacobi run, with a residual that
|
||||
// meets the bound.
|
||||
func TestSparseILUPreconditionedSteps(t *testing.T) {
|
||||
const n = 100
|
||||
coo := triDiagCOO(t, n, -1+0.5, 4, -1)
|
||||
bv := make([]float64, n)
|
||||
for i := range n {
|
||||
bv[i] = math.Sin(float64(i+1) / float64(n+1))
|
||||
}
|
||||
b := mustFloats(t, bv)
|
||||
tol := 1e-10
|
||||
xJ, err := SpSolveBiCGSTAB(coo, b, tol, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Jacobi run: %v", err)
|
||||
}
|
||||
ilu, err := NewSparseILU(coo)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSparseILU: %v", err)
|
||||
}
|
||||
xI, err := SpSolveBiCGSTAB(coo, b, tol, 0, ilu)
|
||||
if err != nil {
|
||||
t.Fatalf("ILU run: %v", err)
|
||||
}
|
||||
for i := range n {
|
||||
if math.Abs(xJ.FloatAt(i)-xI.FloatAt(i)) > 1e-8 {
|
||||
t.Fatalf("solutions disagree at %d: %.14g vs %.14g", i,
|
||||
xJ.FloatAt(i), xI.FloatAt(i))
|
||||
}
|
||||
}
|
||||
// The tridiagonal ILU is the exact LU: the ILU-preconditioned
|
||||
// system must converge in very few steps. Cap the budget where
|
||||
// plain Jacobi needs far more.
|
||||
if _, err := SpSolveBiCGSTAB(coo, b, tol, 3, ilu); err != nil {
|
||||
t.Fatalf("exact-LU preconditioner should converge in 3 steps: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSparseILUErrors(t *testing.T) {
|
||||
if _, err := NewSparseILU(triDiagCOO(t, 3, -1, 0, -1)); err == nil {
|
||||
t.Fatal("zero pivot: want an error")
|
||||
}
|
||||
rect, err := core.NewSparseCOO(mustInts2(t, []int64{0, 0, 0, 1, 0, 2}, 3, 2),
|
||||
floatsToArray([]float64{1, 2, 3}, []int{3}), []int{2, 3})
|
||||
if err != nil {
|
||||
t.Fatalf("NewSparseCOO: %v", err)
|
||||
}
|
||||
if _, err := NewSparseILU(rect); err == nil {
|
||||
t.Fatal("non-square: want an error")
|
||||
}
|
||||
}
|
||||
|
||||
// mustInts2 builds a 2-D int array for sparse constructors.
|
||||
func mustInts2(t *testing.T, vals []int64, rows, cols int) *core.Array {
|
||||
t.Helper()
|
||||
a, err := core.FromInts(vals, rows, cols)
|
||||
if err != nil {
|
||||
t.Fatalf("FromInts: %v", err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
Reference in New Issue
Block a user