Files
tensor/linalg/decompositions_test.go

709 lines
19 KiB
Go
Raw Permalink 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"
"math/rand/v2"
"strings"
"sourcedock.dev/petrbalvin/tensor/internal/core"
"testing"
)
func TestQR(t *testing.T) {
// A 3×2 matrix with a known QR decomposition.
a := mustFromFloats(t, []float64{
12, -51,
6, 167,
-4, 24,
}, 3, 2)
q, r, err := QR(a)
if err != nil {
t.Fatal(err)
}
if q.Shape()[0] != 3 || q.Shape()[1] != 3 {
t.Errorf("QR Q shape: %v", q.Shape())
}
if r.Shape()[0] != 3 || r.Shape()[1] != 2 {
t.Errorf("QR R shape: %v", r.Shape())
}
// QᵀQ should be I.
for i := range 3 {
for j := range 3 {
var want float64
if i == j {
want = 1
}
// QᵀQ: (Qᵀ Q)[i,j] = sum_k Q[k,i] * Q[k,j].
dot := 0.0
for k := range 3 {
qi, _ := core.FloatAt(q, k, i)
qj, _ := core.FloatAt(q, k, j)
dot += qi * qj
}
if math.Abs(dot-want) > 1e-9 {
t.Errorf("QᵀQ[%d,%d]: got %v, want %v", i, j, dot, want)
}
}
}
// R should be upper triangular (entries below the diagonal ~0).
for i := 1; i < 3; i++ {
for j := 0; j < minInt(i, 2); j++ {
v, _ := core.FloatAt(r, i, j)
if math.Abs(v) > 1e-9 {
t.Errorf("R[%d,%d] should be ~0, got %v", i, j, v)
}
}
}
// Q * R should reconstruct A.
recon := mustFromFloats(t, []float64{0, 0, 0, 0, 0, 0}, 3, 2)
for i := range 3 {
for j := range 2 {
sum := 0.0
for k := range 3 {
qik, _ := core.FloatAt(q, i, k)
rkj, _ := core.FloatAt(r, k, j)
sum += qik * rkj
}
recon.RawFloats()[i*2+j] = sum
}
}
for i := range 3 {
for j := range 2 {
got, _ := core.FloatAt(recon, i, j)
orig, _ := core.FloatAt(a, i, j)
if math.Abs(got-orig) > 1e-9 {
t.Errorf("QR reconstruction [%d,%d]: got %v, want %v", i, j, got, orig)
}
}
}
// Square matrix case.
a2 := mustFromFloats(t, []float64{
2, 1,
1, 3,
}, 2, 2)
q2, r2, err := QR(a2)
if err != nil {
t.Fatal(err)
}
if q2.Shape()[0] != 2 || q2.Shape()[1] != 2 {
t.Errorf("QR square Q shape: %v", q2.Shape())
}
if r2.Shape()[0] != 2 || r2.Shape()[1] != 2 {
t.Errorf("QR square R shape: %v", r2.Shape())
}
// Shape errors.
bad, _ := core.FromFloats([]float64{1, 2, 3, 4, 5, 6}, 2, 3)
if _, _, err := QR(bad); err == nil {
t.Error("QR: expected error for m < n")
}
_, _, err = QR(mustFromFloats(t, []float64{1, 2, 3, 4}, 4)) // 1-D
if err == nil {
t.Error("QR: expected error for 1-D input")
}
}
func TestCholesky(t *testing.T) {
// A symmetric positive definite matrix.
a := mustFromFloats(t, []float64{
4, 12, -16,
12, 37, -43,
-16, -43, 98,
}, 3, 3)
l, err := Cholesky(a)
if err != nil {
t.Fatal(err)
}
if l.Shape()[0] != 3 || l.Shape()[1] != 3 {
t.Errorf("Cholesky L shape: %v", l.Shape())
}
// L * Lᵀ should reconstruct A.
for i := range 3 {
for j := range 3 {
sum := 0.0
for k := 0; k <= minInt(i, j); k++ {
lik, _ := core.FloatAt(l, i, k)
ljk, _ := core.FloatAt(l, j, k)
sum += lik * ljk
}
orig, _ := core.FloatAt(a, i, j)
if math.Abs(sum-orig) > 1e-9 {
t.Errorf("Cholesky reconstruction [%d,%d]: got %v, want %v", i, j, sum, orig)
}
}
}
// Non-PD matrix: singular means an error.
notPD := mustFromFloats(t, []float64{1, 2, 2, 1}, 2, 2)
if _, err := Cholesky(notPD); err == nil {
t.Error("Cholesky: expected error for non-PD input")
}
}
func TestSVD(t *testing.T) {
// 3×2 rank-2 matrix with known singular values.
a := mustFromFloats(t, []float64{
1, 0,
0, 2,
0, 0,
}, 3, 2)
u, sigma, vt, err := SVD(a)
if err != nil {
t.Fatalf("SVD: %v", err)
}
if u.Shape()[0] != 3 || u.Shape()[1] != 2 {
t.Errorf("U shape: %v", u.Shape())
}
if sigma.Shape()[0] != 2 {
t.Errorf("Σ shape: %v", sigma.Shape())
}
if vt.Shape()[0] != 2 || vt.Shape()[1] != 2 {
t.Errorf("Vᵀ shape: %v", vt.Shape())
}
// Singular values should be {2, 1} in descending order.
if math.Abs(sigma.RawFloats()[0]-2) > 1e-9 {
t.Errorf("σ₀: got %g want 2", sigma.RawFloats()[0])
}
if math.Abs(sigma.RawFloats()[1]-1) > 1e-9 {
t.Errorf("σ₁: got %g want 1", sigma.RawFloats()[1])
}
if _, _, _, err := SVD(mustFromFloats(t, []float64{1, 2}, 2)); err == nil {
t.Error("SVD: expected error for 1-D input")
}
}
func TestEigen(t *testing.T) {
// Simple diagonal symmetric matrix.
a := mustFromFloats(t, []float64{
4, 0,
0, 9,
}, 2, 2)
vals, vecs, err := Eigen(a)
if err != nil {
t.Fatalf("Eigen: %v", err)
}
if len(vals.RawFloats()) != 2 {
t.Fatalf("vals shape: %v", vals.Shape())
}
if math.Abs(vals.RawFloats()[0]-4) > 1e-9 || math.Abs(vals.RawFloats()[1]-9) > 1e-9 {
t.Errorf("eigenvalues: got %v want [4, 9]", vals.RawFloats())
}
if vecs.Shape()[0] != 2 || vecs.Shape()[1] != 2 {
t.Errorf("vecs shape: %v", vecs.Shape())
}
// 1-D input errors.
if _, _, err := Eigen(mustFromFloats(t, []float64{1, 2}, 2)); err == nil {
t.Error("Eigen: expected error for 1-D input")
}
}
func TestLeastSquares(t *testing.T) {
// Solve [[3, 1], [1, 2]] x = [9, 8]: solution is x = [2, 3].
a := mustFromFloats(t, []float64{
3, 1,
1, 2,
}, 2, 2)
b := mustFromFloats(t, []float64{9, 8}, 2)
x, err := LeastSquares(a, b)
if err != nil {
t.Fatal(err)
}
if x.NDim() != 1 || x.Len() != 2 {
t.Errorf("LeastSquares x shape: %v", x.Shape())
}
v0, _ := core.FloatAt(x, 0)
v1, _ := core.FloatAt(x, 1)
if math.Abs(v0-2) > 1e-9 {
t.Errorf("LeastSquares [0]: got %v, want 2", v0)
}
if math.Abs(v1-3) > 1e-9 {
t.Errorf("LeastSquares [1]: got %v, want 3", v1)
}
// Over-determined system: 3×2 with rank 2.
overA := mustFromFloats(t, []float64{
1, 1,
1, 2,
1, 3,
}, 3, 2)
overB := mustFromFloats(t, []float64{1, 2, 2}, 3)
xOver, err := LeastSquares(overA, overB)
if err != nil {
t.Fatal(err)
}
// Solution minimises ||Ax - b||₂.
if xOver.Len() != 2 {
t.Errorf("LeastSquares over: shape = %v", xOver.Shape())
}
// Shape errors.
if _, err := LeastSquares(mustFromFloats(t, []float64{1, 2, 3}, 3), overB); err == nil {
t.Error("LeastSquares: expected error for 1-D 'a'")
}
underA := mustFromFloats(t, []float64{
1, 2, 3,
4, 5, 6,
}, 2, 3)
if _, err := LeastSquares(underA, overB); err == nil {
t.Error("LeastSquares: expected error for m < n")
}
}
func TestMatrixRank(t *testing.T) {
// Identity has full rank.
id, _ := core.Identity(core.Float, 4)
got, err := MatrixRank(id, 0)
if err != nil {
t.Fatalf("MatrixRank: %v", err)
}
if got != 4 {
t.Errorf("MatrixRank(I_4): got %d want 4", got)
}
if _, err := MatrixRank(mustFromFloats(t, []float64{1, 2}, 2), 0); err == nil {
t.Error("MatrixRank: expected error for 1-D input")
}
}
func TestCond(t *testing.T) {
id, _ := core.Identity(core.Float, 3)
c, err := Cond(id, 0)
if err != nil {
t.Fatalf("Cond: %v", err)
}
if math.Abs(c-1) > 1e-9 {
t.Errorf("Cond(I_3): got %g want 1", c)
}
if _, err := Cond(mustFromFloats(t, []float64{1, 2}, 2), 0); err == nil {
t.Error("Cond: expected error for 1-D input")
}
}
func TestEinsumMatmul(t *testing.T) {
a := mustFromFloats(t, []float64{1, 2, 3, 4}, 2, 2)
b := mustFromFloats(t, []float64{5, 6, 7, 8}, 2, 2)
got, err := core.Einsum("ij,jk->ik", a, b)
if err != nil {
t.Fatal(err)
}
// Result should be MatMul(a, b) = [[19, 22], [43, 50]].
expect := mustFromFloats(t, []float64{19, 22, 43, 50}, 2, 2)
for i := range 4 {
v, _ := core.FloatAt(got, i/2, i%2)
w, _ := core.FloatAt(expect, i/2, i%2)
if math.Abs(v-w) > 1e-9 {
t.Errorf("einsum matmul [%d]: got %v, want %v", i, v, w)
}
}
}
func TestEinsumDot(t *testing.T) {
a := mustFromFloats(t, []float64{1, 2, 3}, 3)
b := mustFromFloats(t, []float64{4, 5, 6}, 3)
got, err := core.Einsum("i,i->", a, b)
if err != nil {
t.Fatal(err)
}
v, _ := core.FloatAt(got, 0)
if math.Abs(v-32) > 1e-9 {
t.Errorf("einsum dot: got %v, want 32", v)
}
}
func TestEinsumTranspose(t *testing.T) {
a := mustFromFloats(t, []float64{1, 2, 3, 4}, 2, 2)
got, err := core.Einsum("ij->ji", a)
if err != nil {
t.Fatal(err)
}
// Transpose: [[1,2],[3,4]] -> [[1,3],[2,4]]
v00, _ := core.FloatAt(got, 0, 0)
v01, _ := core.FloatAt(got, 0, 1)
v10, _ := core.FloatAt(got, 1, 0)
v11, _ := core.FloatAt(got, 1, 1)
if v00 != 1 || v01 != 3 || v10 != 2 || v11 != 4 {
t.Errorf("einsum transpose: got [[%v,%v],[%v,%v]], want [[1,3],[2,4]]", v00, v01, v10, v11)
}
}
func TestEinsumDiagonal(t *testing.T) {
a := mustFromFloats(t, []float64{
1, 2, 3,
4, 5, 6,
7, 8, 9,
}, 3, 3)
got, err := core.Einsum("ii->i", a)
if err != nil {
t.Fatal(err)
}
v0, _ := core.FloatAt(got, 0)
v1, _ := core.FloatAt(got, 1)
v2, _ := core.FloatAt(got, 2)
if v0 != 1 || v1 != 5 || v2 != 9 {
t.Errorf("einsum diagonal: got %v %v %v, want 1 5 9", v0, v1, v2)
}
}
func TestEinsumTrace(t *testing.T) {
a := mustFromFloats(t, []float64{
1, 2, 3,
4, 5, 6,
7, 8, 9,
}, 3, 3)
got, err := core.Einsum("ii->", a)
if err != nil {
t.Fatal(err)
}
v, _ := core.FloatAt(got, 0)
if v != 15 { // 1 + 5 + 9
t.Errorf("einsum trace: got %v, want 15", v)
}
}
func TestEinsumSum(t *testing.T) {
a := mustFromFloats(t, []float64{1, 2, 3, 4}, 2, 2)
got, err := core.Einsum("ij->", a)
if err != nil {
t.Fatal(err)
}
v, _ := core.FloatAt(got, 0)
if v != 10 {
t.Errorf("einsum sum: got %v, want 10", v)
}
}
func TestEinsumElementWiseMul(t *testing.T) {
a := mustFromFloats(t, []float64{1, 2, 3, 4}, 2, 2)
b := mustFromFloats(t, []float64{5, 6, 7, 8}, 2, 2)
got, err := core.Einsum("ij,ij->ij", a, b)
if err != nil {
t.Fatal(err)
}
for i, w := range []float64{5, 12, 21, 32} {
v, _ := core.FloatAt(got, i/2, i%2)
if v != w {
t.Errorf("einsum elwise [%d]: got %v, want %v", i, v, w)
}
}
}
func TestEinsumOuter(t *testing.T) {
a := mustFromFloats(t, []float64{1, 2}, 2)
b := mustFromFloats(t, []float64{3, 4, 5}, 3)
got, err := core.Einsum("i,j->ij", a, b)
if err != nil {
t.Fatal(err)
}
if got.Shape()[0] != 2 || got.Shape()[1] != 3 {
t.Errorf("einsum outer shape: %v", got.Shape())
}
// [1*3, 1*4, 1*5; 2*3, 2*4, 2*5] = [[3,4,5],[6,8,10]]
expect := []float64{3, 4, 5, 6, 8, 10}
for i, w := range expect {
v, _ := core.FloatAt(got, i/3, i%3)
if v != w {
t.Errorf("einsum outer [%d]: got %v, want %v", i, v, w)
}
}
}
// TestEinsumTransposedInner pins the label alignment of full
// contractions: "ij,ji->" must pair a's columns with b's rows. It used
// to multiply positionally, computing "ij,ij->" and failing outright
// on non-square shapes.
func TestEinsumTransposedInner(t *testing.T) {
a := mustFromFloats(t, []float64{1, 2, 3, 4}, 2, 2)
b := mustFromFloats(t, []float64{5, 6, 7, 8}, 2, 2)
got, err := core.Einsum("ij,ji->", a, b)
if err != nil {
t.Fatal(err)
}
v, _ := core.FloatAt(got, 0)
if v != 69 { // 1*5 + 2*7 + 3*6 + 4*8
t.Errorf("einsum ij,ji->: got %v, want 69", v)
}
// Non-square operands must work: sum of a * b^T.
w := mustFromFloats(t, []float64{1, 2, 3, 4, 5, 6}, 2, 3)
x := mustFromFloats(t, []float64{7, 8, 9, 10, 11, 12}, 3, 2)
got2, err := core.Einsum("ij,ji->", w, x)
if err != nil {
t.Fatal(err)
}
v2, _ := core.FloatAt(got2, 0)
if v2 != 212 { // 58 + 154, the sum of w * x^T
t.Errorf("einsum ij,ji-> non-square: got %v, want 212", v2)
}
}
// TestEinsumOuterDtypes pins the promotion ladder of the outer product:
// int vectors stay int (exact products), complex vectors stay complex.
func TestEinsumOuterDtypes(t *testing.T) {
ia := mustFromInts(t, []int64{1, 2}, 2)
ib := mustFromInts(t, []int64{3, 4}, 2)
got, err := core.Einsum("i,j->ij", ia, ib)
if err != nil {
t.Fatal(err)
}
if got.Dtype() != core.Int {
t.Fatalf("einsum outer int dtype: %s", got.Dtype())
}
want := mustFromInts(t, []int64{3, 4, 6, 8}, 2, 2)
if !core.Equal(want, got) {
t.Errorf("einsum outer int: %s", got)
}
ca := mustFromComplexes(t, []complex128{1 + 2i}, 1)
cb := mustFromComplexes(t, []complex128{3 + 4i}, 1)
gotC, err := core.Einsum("i,j->ij", ca, cb)
if err != nil {
t.Fatal(err)
}
if gotC.Dtype() != core.Complex {
t.Fatalf("einsum outer complex dtype: %s", gotC.Dtype())
}
vc, err := core.ComplexAt(gotC, 0, 0)
if err != nil {
t.Fatal(err)
}
if vc != -5+10i { // (1+2i)(3+4i)
t.Errorf("einsum outer complex: got %v, want (-5+10i)", vc)
}
}
func TestEinsumError(t *testing.T) {
a := mustFromFloats(t, []float64{1, 2}, 2)
// Wrong operand count.
if _, err := core.Einsum("i,j->ij", a); err == nil {
t.Error("Einsum: expected error for wrong operand count")
}
// Unsupported pattern.
b := mustFromFloats(t, []float64{3, 4}, 2)
if _, err := core.Einsum("ii,jj->ij", a, b); err == nil {
t.Error("Einsum: expected error for unsupported pattern")
}
// Bad spec (no ->).
if _, err := core.Einsum("ii", a); err == nil {
t.Error("Einsum: expected error for missing ->")
}
}
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
// TestEigenNonDiagonalReconstruction pins the corrected symmetric QR
// sweep: eigenpairs of non-diagonal matrices used to come back as
// garbage (near-zero eigenvalues for [[2,1],[1,2]]) because the QR step
// updated the tridiagonal with incoherent formulas. Eigenpairs are now
// checked by the residual ‖A·v − λ·v‖ and eigenvector orthogonality
// across a range of ranks.
func TestEigenNonDiagonalReconstruction(t *testing.T) {
// Golden: eigenvalues of [[2,1],[1,2]] are 1 and 3.
em := mustFromFloats(t, []float64{2, 1, 1, 2}, 2, 2)
vals, _, err := Eigen(em)
if err != nil {
t.Fatalf("Eigen golden: %v", err)
}
if v := vals.FloatAt(0); math.Abs(v-1) > 1e-12 {
t.Errorf("Eigen golden [0]: %v, want 1", v)
}
if v := vals.FloatAt(1); math.Abs(v-3) > 1e-12 {
t.Errorf("Eigen golden [1]: %v, want 3", v)
}
for _, n := range []int{3, 5, 8, 20, 50} {
mat := make([]float64, n*n)
for i := range n {
for j := i; j < n; j++ {
v := math.Sin(float64(n)+float64(i*7+j*13)) * 2.0
mat[i*n+j] = v
mat[j*n+i] = v
}
}
a := floatsToArray(mat, []int{n, n})
vs, vc, err := Eigen(a)
if err != nil {
t.Fatalf("Eigen n=%d: %v", n, err)
}
maxRes, maxOrtho := 0.0, 0.0
for k := range n {
for i := range n {
av := 0.0
for j := range n {
av += mat[i*n+j] * vc.FloatAt(j*n+k)
}
if d := math.Abs(av - vs.FloatAt(k)*vc.FloatAt(i*n+k)); d > maxRes {
maxRes = d
}
}
for k2 := k + 1; k2 < n; k2++ {
dot := 0.0
for i := range n {
dot += vc.FloatAt(i*n+k) * vc.FloatAt(i*n+k2)
}
if math.Abs(dot) > maxOrtho {
maxOrtho = math.Abs(dot)
}
}
}
if maxRes > 1e-11 || maxOrtho > 1e-12 {
t.Errorf("Eigen n=%d: residual %v, orthogonality %v", n, maxRes, maxOrtho)
}
}
}
// TestSVDGeneralReconstruction pins the corrected Golub-Kahan
// pipeline: the old bidiagonalisation zeroed the wrong direction and
// dropped the upper triangle, the right singular vectors ignored V₁,
// and the wide return path transposed U. General matrices of every
// aspect ratio must now reconstruct with orthonormal factors.
func TestSVDGeneralReconstruction(t *testing.T) {
for _, tc := range []struct{ m, n int }{{4, 4}, {6, 3}, {3, 6}, {8, 5}, {5, 8}, {2, 3}} {
m, n := tc.m, tc.n
mat := make([]float64, m*n)
for i := range mat {
mat[i] = math.Sin(float64(i)*0.7)*1.5 + 0.3
}
a := floatsToArray(mat, []int{m, n})
u, s, vt, err := SVD(a)
if err != nil {
t.Fatalf("SVD %dx%d: %v", m, n, err)
}
if got := s.Shape()[0]; got != min(m, n) {
t.Fatalf("SVD %dx%d: %d singular values, want %d", m, n, got, min(m, n))
}
ucols := u.Shape()[1]
if ucols != min(m, n) || vt.Shape()[0] != min(m, n) {
t.Fatalf("SVD %dx%d thin shapes: u=%v vt=%v", m, n, u.Shape(), vt.Shape())
}
maxRec := 0.0
for i := range m {
for j := range n {
sum := 0.0
for k := range s.Len() {
sum += u.FloatAt(i*ucols+k) * s.FloatAt(k) * vt.FloatAt(k*n+j)
}
if d := math.Abs(sum - mat[i*n+j]); d > maxRec {
maxRec = d
}
}
}
orthU := 0.0
for k1 := range ucols {
for k2 := range ucols {
dot := 0.0
for i := range m {
dot += u.FloatAt(i*ucols+k1) * u.FloatAt(i*ucols+k2)
}
want := 0.0
if k1 == k2 {
want = 1
}
if d := math.Abs(dot - want); d > orthU {
orthU = d
}
}
}
if maxRec > 1e-9 || orthU > 1e-12 {
t.Errorf("SVD %dx%d: reconstruction %v, U orthogonality %v", m, n, maxRec, orthU)
}
// Σ must come back descending.
for k := 1; k < s.Len(); k++ {
if s.FloatAt(k) > s.FloatAt(k-1) {
t.Errorf("SVD %dx%d: σ not descending at %d", m, n, k)
}
}
}
}
// TestFitPolynomialRefusesExtremeDegree pins the degree guard: degree
// MaxInt would wrap degree+1 negative and slip past the sample count
// check.
func TestFitPolynomialRefusesExtremeDegree(t *testing.T) {
x := mustFloats(t, []float64{0, 1, 2}, 3)
y := mustFloats(t, []float64{0, 1, 4}, 3)
if _, err := FitPolynomial(x, y, math.MaxInt); err == nil || !strings.Contains(err.Error(), "too large") {
t.Fatalf("FitPolynomial with degree MaxInt: %v", err)
}
}
// TestCholeskyBlockedReconstruction pins the factorisation past the
// width of its column block, the sizes only the benchmarks otherwise
// reach: 320 is five whole blocks and 200 three blocks and a partial
// one, so the panel update, the diagonal block and the trapezoid divide
// all run, the last of them over a block that does not end on a
// boundary. A divide that reaches back over the finished columns, or a
// panel update that misses them, leaves L·Lᵀ away from A by far more
// than rounding, so the reconstruction is checked against the original
// matrix rather than against another run of the same sweep.
func TestCholeskyBlockedReconstruction(t *testing.T) {
for _, n := range []int{320, 200, 96} {
rng := rand.New(rand.NewPCG(3, 5))
b := make([]float64, n*n)
for i := range b {
b[i] = rng.NormFloat64()
}
// A = B·Bᵀ + n·I is symmetric positive definite, with the
// diagonal held well above the rounding floor.
a := make([]float64, n*n)
for i := range n {
for j := range i + 1 {
s := 0.0
for k := range n {
s += b[i*n+k] * b[j*n+k]
}
if i == j {
s += float64(n)
}
a[i*n+j], a[j*n+i] = s, s
}
}
l, err := Cholesky(mustFromFloats(t, a, n, n))
if err != nil {
t.Fatalf("Cholesky(%d): %v", n, err)
}
lf := l.RawFloats()
worst, scale := 0.0, 0.0
for i := range n {
if lf[i*n+i] <= 0 {
t.Fatalf("n=%d: the factor's diagonal at %d is %g, want a positive real root", n, i, lf[i*n+i])
}
for j := range n {
if j > i && lf[i*n+j] != 0 {
t.Fatalf("n=%d: the factor holds %g above the diagonal at [%d,%d]", n, lf[i*n+j], i, j)
}
s := 0.0
for k := range min(i, j) + 1 {
s += lf[i*n+k] * lf[j*n+k]
}
if d := math.Abs(s - a[i*n+j]); d > worst {
worst = d
}
if v := math.Abs(a[i*n+j]); v > scale {
scale = v
}
}
}
if worst/scale > 1e-12 {
t.Fatalf("n=%d: L·Lᵀ misses A by %.6g (scale %.6g), relative %.3g", n, worst, scale, worst/scale)
}
}
}
// TestCholeskyRefusesNonFinite pins the refusal of a poisoned matrix:
// the pivot test cannot see a NaN (it fails every comparison), so
// without the gate the sweep would answer an all-NaN factor with a nil
// error, where the sparse sibling refuses the same input.
func TestCholeskyRefusesNonFinite(t *testing.T) {
nan := mustFromFloats(t, []float64{math.NaN(), 0, 0, 1}, 2, 2)
if _, err := Cholesky(nan); err == nil || !strings.Contains(err.Error(), "not finite") {
t.Fatalf("Cholesky(NaN): %v", err)
}
inf := mustFromFloats(t, []float64{0, 0, 0, math.Inf(1)}, 2, 2)
if _, err := Cholesky(inf); err == nil || !strings.Contains(err.Error(), "not finite") {
t.Fatalf("Cholesky(Inf): %v", err)
}
}