198 lines
5.4 KiB
Go
198 lines
5.4 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: MIT
|
||
|
|
|
||
|
|
package linalg
|
||
|
|
|
||
|
|
import (
|
||
|
|
"math"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"sourcedock.dev/petrbalvin/tensor/internal/core"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Regression pins for non-finite and counting guards: the matrix
|
||
|
|
// exponential refuses a non-finite entry, the minimum-degree ordering
|
||
|
|
// matches its brute-force reference, the sparse LU non-zero counts
|
||
|
|
// follow the unit triangle, and the ILU intake refuses overflow.
|
||
|
|
|
||
|
|
// TestMatrixExpRejectsNonFinite pins the loud refusal for a
|
||
|
|
// non-finite entry, which the theta ladder used to read through an
|
||
|
|
// implementation-defined conversion into an all-NaN answer.
|
||
|
|
func TestMatrixExpRejectsNonFinite(t *testing.T) {
|
||
|
|
a := mustF(t, []float64{math.Inf(1), 0, 0, 1}, 2, 2)
|
||
|
|
if _, err := MatrixExp(a); err == nil {
|
||
|
|
t.Fatal("expected an error for an Inf entry")
|
||
|
|
}
|
||
|
|
b := mustF(t, []float64{math.NaN(), 0, 0, 1}, 2, 2)
|
||
|
|
if _, err := MatrixExp(b); err == nil {
|
||
|
|
t.Fatal("expected an error for a NaN entry")
|
||
|
|
}
|
||
|
|
c, _ := core.FromComplexes([]complex128{complex(math.Inf(1), 0), 0, 0, 1}, 2, 2)
|
||
|
|
if _, err := MatrixExp(c); err == nil {
|
||
|
|
t.Fatal("expected an error for an Inf complex entry")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestMinimumDegreeMixedPattern pins the ordering against a
|
||
|
|
// brute-force minimum-degree reference on a mixed-degree pattern,
|
||
|
|
// where the degree-sorted adjacency lists and the index-sorted set
|
||
|
|
// union used to disagree and corrupt the elimination. The sample is
|
||
|
|
// the measured failing case: the pre-fix code eliminated vertex 10
|
||
|
|
// before 1 and swapped the tail of the order.
|
||
|
|
func TestMinimumDegreeMixedPattern(t *testing.T) {
|
||
|
|
rows := []int{0, 0, 0, 0, 1, 1, 1, 1, 2, 4, 5, 5, 6, 6, 7}
|
||
|
|
cols := []int{1, 2, 5, 8, 3, 5, 6, 10, 6, 6, 9, 10, 7, 10, 10}
|
||
|
|
const n = 11
|
||
|
|
coo := edgesCOO(t, rows, cols, n)
|
||
|
|
csc, err := CSCFromCOO(coo)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("CSCFromCOO: %v", err)
|
||
|
|
}
|
||
|
|
got, err := minimumDegree(csc)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("minimumDegree: %v", err)
|
||
|
|
}
|
||
|
|
// Brute-force reference: repeatedly eliminate the uneliminated
|
||
|
|
// vertex with the fewest uneliminated neighbours (ties to the
|
||
|
|
// smaller index), unioning neighbourhoods exactly.
|
||
|
|
adj := map[int]map[int]bool{}
|
||
|
|
addEdge := func(i, j int) {
|
||
|
|
if adj[i] == nil {
|
||
|
|
adj[i] = map[int]bool{}
|
||
|
|
}
|
||
|
|
if adj[j] == nil {
|
||
|
|
adj[j] = map[int]bool{}
|
||
|
|
}
|
||
|
|
adj[i][j], adj[j][i] = true, true
|
||
|
|
}
|
||
|
|
for e := range rows {
|
||
|
|
addEdge(rows[e], cols[e])
|
||
|
|
}
|
||
|
|
eliminated := map[int]bool{}
|
||
|
|
var want []int
|
||
|
|
for range n {
|
||
|
|
best, bestDeg := -1, math.MaxInt
|
||
|
|
for v := range n {
|
||
|
|
if eliminated[v] {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
d := 0
|
||
|
|
for u := range adj[v] {
|
||
|
|
if !eliminated[u] {
|
||
|
|
d++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if d < bestDeg {
|
||
|
|
best, bestDeg = v, d
|
||
|
|
}
|
||
|
|
}
|
||
|
|
want = append(want, best)
|
||
|
|
eliminated[best] = true
|
||
|
|
nb := map[int]bool{}
|
||
|
|
for u := range adj[best] {
|
||
|
|
if !eliminated[u] {
|
||
|
|
nb[u] = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for u := range nb {
|
||
|
|
for w := range nb {
|
||
|
|
if u != w {
|
||
|
|
adj[u][w] = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
delete(adj[u], best)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for i := range n {
|
||
|
|
if got[i] != want[i] {
|
||
|
|
t.Fatalf("order[%d] = %d, want %d (full %v vs %v)", i, got[i], want[i], got, want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestSparseLUNNZCountsUTriangle pins that NNZ includes U's
|
||
|
|
// strict triangle, which the column walk used to miss.
|
||
|
|
func TestSparseLUNNZCountsUTriangle(t *testing.T) {
|
||
|
|
// A tridiagonal matrix: L holds the subdiagonal, U the diagonal and
|
||
|
|
// the superdiagonal, so the factor stores exactly 3n - 2 entries.
|
||
|
|
const n = 8
|
||
|
|
idx := make([]int64, 0, 6*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, 2)
|
||
|
|
if i+1 < n {
|
||
|
|
add(i, i+1, -1)
|
||
|
|
add(i+1, i, -1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
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)
|
||
|
|
}
|
||
|
|
f, err := NewSparseLU(coo)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSparseLU: %v", err)
|
||
|
|
}
|
||
|
|
if want := 3*n - 2; f.NNZ() != want {
|
||
|
|
t.Fatalf("NNZ = %d, want %d (L strict + U strict + diagonal)", f.NNZ(), want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestSparseILUOverflowRefused pins the overflow refusal on
|
||
|
|
// finite input, mirroring the LU and Cholesky guards.
|
||
|
|
func TestSparseILUOverflowRefused(t *testing.T) {
|
||
|
|
idx := make([]int64, 0, 6)
|
||
|
|
vals := make([]float64, 0, 3)
|
||
|
|
add := func(r, c int, v float64) {
|
||
|
|
idx = append(idx, int64(r), int64(c))
|
||
|
|
vals = append(vals, v)
|
||
|
|
}
|
||
|
|
add(0, 0, 1e-200)
|
||
|
|
add(0, 1, 1e100)
|
||
|
|
add(1, 0, 1e100)
|
||
|
|
add(1, 1, 1e200)
|
||
|
|
indices, err := core.FromInts(idx, 4, 2)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("FromInts: %v", err)
|
||
|
|
}
|
||
|
|
coo, err := core.NewSparseCOO(indices, floatsToArray(vals, []int{4}), []int{2, 2})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSparseCOO: %v", err)
|
||
|
|
}
|
||
|
|
if _, err := NewSparseILU(coo); err == nil {
|
||
|
|
t.Fatal("expected an overflow error from the elimination")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// edgesCOO builds a symmetric-pattern COO from edge lists.
|
||
|
|
func edgesCOO(t *testing.T, rows, cols []int, n int) *core.SparseCOO {
|
||
|
|
t.Helper()
|
||
|
|
idx := make([]int64, 0, 2*len(rows))
|
||
|
|
vals := make([]float64, 0, 2*len(rows))
|
||
|
|
add := func(r, c int) {
|
||
|
|
idx = append(idx, int64(r), int64(c))
|
||
|
|
vals = append(vals, 1)
|
||
|
|
}
|
||
|
|
for e := range rows {
|
||
|
|
add(rows[e], cols[e])
|
||
|
|
add(cols[e], rows[e])
|
||
|
|
}
|
||
|
|
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
|
||
|
|
}
|