132 lines
4.1 KiB
Go
132 lines
4.1 KiB
Go
// 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")
|
||
}
|
||
}
|