Files

132 lines
4.1 KiB
Go
Raw Permalink Normal View History

2026-09-03 10:00:00 +02:00
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package linalg
import (
"math"
"sourcedock.dev/petrbalvin/tensor/internal/core"
"testing"
)
// pencilSample builds a deterministic symmetric a and a symmetric
// positive definite b of size n.
func pencilSample(n int) (*core.Array, *core.Array) {
raw := make([]float64, n*n)
for i := range n {
for j := range n {
raw[i*n+j] = math.Sin(float64(2*i + 3*j + 1))
}
}
av := make([]float64, n*n)
bv := make([]float64, n*n)
for i := range n {
for j := range n {
av[i*n+j] = raw[i*n+j] + raw[j*n+i]
bv[i*n+j] = math.Cos(float64(3*i+2*j)) + math.Cos(float64(3*j+2*i))
}
bv[i*n+i] += float64(n) + 1
}
a, _ := core.FromFloats(av, n, n)
b, _ := core.FromFloats(bv, n, n)
return a, b
}
// TestEigenGeneralisedDiagonal pins the pencil on a diagonal pair,
// where the eigenvalues are the entrywise ratios and the eigenvectors
// are the scaled unit vectors.
func TestEigenGeneralisedDiagonal(t *testing.T) {
a := mustFloats(t, []float64{1, 0, 0, 4}, 2, 2)
b := mustFloats(t, []float64{1, 0, 0, 2}, 2, 2)
values, vectors, err := EigenGeneralised(a, b)
if err != nil {
t.Fatalf("EigenGeneralised: %v", err)
}
if math.Abs(values.FloatAt(0)-1) > 1e-12 || math.Abs(values.FloatAt(1)-2) > 1e-12 {
t.Fatalf("values = (%.12g, %.12g), want (1, 2)", values.FloatAt(0), values.FloatAt(1))
}
// First eigenvector: e1 scaled to unit B norm = (1, 0); second:
// e2 with 2·vᵀBv... v = (0, 1/√2) so vᵀBv = (1/2)·2 = 1.
if math.Abs(vectors.FloatAt(0)-1) > 1e-12 || math.Abs(vectors.FloatAt(3)-1/math.Sqrt2) > 1e-12 {
t.Fatalf("vectors = (%.12g, %.12g; %.12g, %.12g), want (1, 0; 0, 1/√2)",
vectors.FloatAt(0), vectors.FloatAt(1), vectors.FloatAt(2), vectors.FloatAt(3))
}
}
// TestEigenGeneralisedResidual checks the defining equations on a
// deterministic pencil: A·X = B·X·Λ to rounding, the vectors
// B-orthonormal, the values ascending.
func TestEigenGeneralisedResidual(t *testing.T) {
n := 6
a, b := pencilSample(n)
values, vectors, err := EigenGeneralised(a, b)
if err != nil {
t.Fatalf("EigenGeneralised: %v", err)
}
scale := 0.0
for i := range a.Len() {
scale = math.Max(scale, math.Abs(a.FloatAt(i))+math.Abs(b.FloatAt(i)))
}
worst := 0.0
for j := range n {
lam := values.FloatAt(j)
for i := range n {
// (A·X − λ·B·X) at row i, column j.
ax, bx := 0.0, 0.0
for k := range n {
ax += a.FloatAt(i*n+k) * vectors.FloatAt(k*n+j)
bx += b.FloatAt(i*n+k) * vectors.FloatAt(k*n+j)
}
worst = math.Max(worst, math.Abs(ax-lam*bx))
}
}
if worst > 1e-8*scale {
t.Fatalf("pencil residual %.3g exceeds %.3g", worst, 1e-8*scale)
}
// B-orthonormality: Xᵀ·B·X = I.
for j := range n {
for i := j; i < n; i++ {
s := 0.0
for k := range n {
for l := range n {
s += vectors.FloatAt(k*n+i) * b.FloatAt(k*n+l) * vectors.FloatAt(l*n+j)
}
}
want := 0.0
if i == j {
want = 1
}
if math.Abs(s-want) > 1e-8 {
t.Fatalf("XᵀBX[%d][%d] = %.12g, want %.12g", i, j, s, want)
}
}
}
for i := 1; i < n; i++ {
if values.FloatAt(i) < values.FloatAt(i-1) {
t.Fatalf("values not ascending at %d", i)
}
}
}
// TestEigenGeneralisedErrors pins the validation contract.
func TestEigenGeneralisedErrors(t *testing.T) {
a, b := pencilSample(3)
cx, _ := core.FromComplexes([]complex128{1, 0, 0, 1}, 2, 2)
if _, _, err := EigenGeneralised(cx, b); err == nil {
t.Fatal("expected an error for a complex pencil")
}
bad, _ := core.FromFloats([]float64{1, 2, 3, 4, 5, 6}, 2, 3)
if _, _, err := EigenGeneralised(a, bad); err == nil {
t.Fatal("expected an error for mismatched sizes")
}
rect, _ := core.FromFloats([]float64{1, 2, 3, 4, 5, 6}, 3, 2)
if _, _, err := EigenGeneralised(rect, b); err == nil {
t.Fatal("expected an error for a rectangular a")
}
// b = all-ones is symmetric but singular: the Cholesky must refuse.
singular, _ := core.FromFloats([]float64{1, 1, 1, 1}, 2, 2)
if _, _, err := EigenGeneralised(mustFloats(t, []float64{1, 0, 0, 1}, 2, 2), singular); err == nil {
t.Fatal("expected an error for a singular b")
}
}