709 lines
19 KiB
Go
709 lines
19 KiB
Go
// 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)
|
||
}
|
||
}
|