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