feat: initial release
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user