// Copyright (c) 2026 Petr BalvĂ­n (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 }