Files
tensor/linalg/sparsebicg_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

196 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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"
)
// TestSpSolveBiCGSTABNonsymmetric checks the solver against the dense
// Solve on a 4×4 system with genuinely asymmetric entries.
func TestSpSolveBiCGSTABNonsymmetric(t *testing.T) {
dense := [][]float64{
{10, 1, 0, 2},
{-1, 12, 3, 0},
{0, -2, 15, 1},
{2, 0, -1, 8},
}
entries := make([][3]float64, 0, 16)
for i := range 4 {
for j := range 4 {
if dense[i][j] != 0 {
entries = append(entries, [3]float64{float64(i), float64(j), dense[i][j]})
}
}
}
idx := make([]int64, 0, len(entries))
vals := make([]float64, 0, len(entries))
for _, e := range entries {
idx = append(idx, int64(e[0]), int64(e[1]))
vals = append(vals, e[2])
}
indices, err := core.FromInts(idx, len(entries), 2)
if err != nil {
t.Fatalf("FromInts: %v", err)
}
coo, err := core.NewSparseCOO(indices, floatsToArray(vals, []int{len(entries)}), []int{4, 4})
if err != nil {
t.Fatalf("NewSparseCOO: %v", err)
}
b := mustFloats(t, []float64{1, 2, 3, 4})
x, err := SpSolveBiCGSTAB(coo, b, 1e-12, 0)
if err != nil {
t.Fatalf("SpSolveBiCGSTAB: %v", err)
}
ref, err := Solve(mustFloats(t, []float64{
10, 1, 0, 2,
-1, 12, 3, 0,
0, -2, 15, 1,
2, 0, -1, 8,
}, 4, 4), b)
if err != nil {
t.Fatalf("Solve: %v", err)
}
for i := range 4 {
if math.Abs(x.FloatAt(i)-ref.FloatAt(i)) > 1e-9 {
t.Fatalf("x[%d] = %.14g, want %.14g", i, x.FloatAt(i), ref.FloatAt(i))
}
}
}
// TestSpSolveBiCGSTABConvective builds a 50×50 shifted Laplacian with
// an asymmetric convection term, the shape transport discretisations
// produce, and checks the residual the solver promises.
func TestSpSolveBiCGSTABConvective(t *testing.T) {
const n = 50
idx := make([]int64, 0, 4*n)
vals := make([]float64, 0, 4*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, 4)
if i+1 < n {
add(i, i+1, -1)
add(i+1, i, -1+0.5) // asymmetric neighbour coupling
}
}
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)
}
bv := make([]float64, n)
for i := range n {
bv[i] = float64(i+1) / float64(n+1)
}
b := mustFloats(t, bv)
x, err := SpSolveBiCGSTAB(coo, b, 1e-10, 0)
if err != nil {
t.Fatalf("SpSolveBiCGSTAB: %v", err)
}
// Residual check against the sparse product path.
ax, err := cooToCSRForTest(t, coo).MatVec(x)
if err != nil {
t.Fatalf("MatVec: %v", err)
}
res := 0.0
for i := range n {
d := b.FloatAt(i) - ax.FloatAt(i)
res += d * d
}
if math.Sqrt(res) > 1e-9*math.Sqrt(dotF64(bv, bv)) {
t.Fatalf("relative residual %g exceeds 1e-9", math.Sqrt(res)/math.Sqrt(dotF64(bv, bv)))
}
}
// cooToCSRForTest converts a COO to the public CSR for residual checks.
func cooToCSRForTest(t *testing.T, coo *core.SparseCOO) *SparseCSR {
t.Helper()
csr, err := CSRFromCOO(coo)
if err != nil {
t.Fatalf("CSRFromCOO: %v", err)
}
return csr
}
// TestSpSolveBiCGSTABConvergenceFailure reports an honest failure when
// the budget cannot reach the tolerance.
func TestSpSolveBiCGSTABConvergenceFailure(t *testing.T) {
const n = 30
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, 4)
if i+1 < n {
add(i, i+1, -1)
add(i+1, i, -0.5)
}
}
indices, _ := core.FromInts(idx, len(vals), 2)
coo, err := core.NewSparseCOO(indices, floatsToArray(vals, []int{len(vals)}), []int{n, n})
if err != nil {
t.Fatalf("NewSparseCOO: %v", err)
}
b := mustFloats(t, make([]float64, n))
for i := range n {
b.RawFloats()[i] = 1
}
if _, err := SpSolveBiCGSTAB(coo, b, 1e-14, 2); err == nil {
t.Fatal("tight tolerance with a two-step budget: want an error")
}
}
// TestSpSolveBiCGSTABErrors pins the validation surface.
func TestSpSolveBiCGSTABErrors(t *testing.T) {
// Build the COO inputs directly for the error paths.
mkCOO := func(t *testing.T, rows, cols int, entries [][3]float64) *core.SparseCOO {
t.Helper()
idx := make([]int64, 0, len(entries)*2)
vals := make([]float64, 0, len(entries))
for _, e := range entries {
idx = append(idx, int64(e[0]), int64(e[1]))
vals = append(vals, e[2])
}
indices, err := core.FromInts(idx, len(entries), 2)
if err != nil {
t.Fatalf("FromInts: %v", err)
}
coo, err := core.NewSparseCOO(indices, floatsToArray(vals, []int{len(entries)}), []int{rows, cols})
if err != nil {
t.Fatalf("NewSparseCOO: %v", err)
}
return coo
}
b2 := mustFloats(t, []float64{1, 1})
if _, err := SpSolveBiCGSTAB(mkCOO(t, 2, 3, [][3]float64{{0, 0, 2}}), b2, 0, 0); err == nil {
t.Fatal("non-square matrix: want an error")
}
// A missing diagonal entry defeats the Jacobi preconditioner.
gap := mkCOO(t, 2, 2, [][3]float64{{0, 1, 1}, {1, 1, 3}})
if _, err := SpSolveBiCGSTAB(gap, b2, 0, 0); err == nil {
t.Fatal("missing diagonal: want an error")
}
bad := mkCOO(t, 2, 2, [][3]float64{{0, 0, 2}, {1, 1, 3}})
if _, err := SpSolveBiCGSTAB(bad, mustFloats(t, []float64{1, 2, 3}), 0, 0); err == nil {
t.Fatal("wrong right-hand side length: want an error")
}
complexVals := core.New(core.Complex, 2)
cx := mkCOO(t, 2, 2, [][3]float64{{0, 0, 1}, {1, 1, 1}})
cx.Values = complexVals
if _, err := SpSolveBiCGSTAB(cx, b2, 0, 0); err == nil {
t.Fatal("complex values: want an error")
}
}