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