feat: initial release
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-03 10:00:00 +02:00
commit af4ee19703
617 changed files with 191195 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package linalg
import (
"sourcedock.dev/petrbalvin/tensor/internal/base"
"sourcedock.dev/petrbalvin/tensor/internal/core"
)
// The generalised symmetric eigenproblem A·v = λ·B·v, the standard form
// of vibrating-system and covariance questions: the eigenvalues of the
// pencil (A, B) with B symmetric positive definite. The Cholesky route
// reduces it to the ordinary symmetric problem without ever forming
// B⁻¹A, whose asymmetry would square the conditioning.
// EigenGeneralised solves A·v = λ·B·v for a symmetric a and a
// symmetric positive definite b, both real n×n. b = L·Lᵀ turns the
// pencil into the standard symmetric problem for C = L⁻¹·A·L⁻ᵀ, which
// shares the eigenvalues; its ordinary eigenvectors y transform back as
// v = L⁻ᵀ·y, which lands them B-orthonormal (vᵀ·B·v = 1) for free.
// Values come back ascending in a 1-D array with the eigenvectors as
// the matching columns, the convention Eigen uses. A complex input, a
// size mismatch, or a b that fails its Cholesky factorisation is an
// error; a itself must be symmetric, which is not verified.
func EigenGeneralised(a, b *core.Array) (values, vectors *core.Array, err error) {
const name = "EigenGeneralised"
if a.Dtype() == core.Complex || b.Dtype() == core.Complex {
return nil, nil, base.Errf("%s: complex pencils are not supported", name)
}
if a.NDim() != 2 || a.Shape()[0] != a.Shape()[1] {
return nil, nil, base.Errf("%s: a must be a square 2-D matrix, got shape %s", name, base.ShapeText(a.Shape()))
}
if b.NDim() != 2 || b.Shape()[0] != b.Shape()[1] {
return nil, nil, base.Errf("%s: b must be a square 2-D matrix, got shape %s", name, base.ShapeText(b.Shape()))
}
n := a.Shape()[0]
if b.Shape()[0] != n {
return nil, nil, base.Errf("%s: size mismatch, a is %d×%d and b is %d×%d",
name, n, n, b.Shape()[0], b.Shape()[1])
}
if n == 0 {
return nil, nil, base.Errf("%s: zero-sized pencil", name)
}
l, err := Cholesky(b)
if err != nil {
return nil, nil, base.Errf("%s: %w", name, err)
}
lFlat := denseFloats(l, n, n)
// solveSystem consumes its matrix in place, so each solve gets a
// fresh copy of L's rows as views over one flat backing slice.
freshRows := func() [][]float64 {
back := make([]float64, n*n)
copy(back, lFlat)
rows := make([][]float64, n)
for i := range n {
rows[i] = back[i*n : (i+1)*n]
}
return rows
}
// X = L⁻¹·A, one column of a per right-hand side.
aCols := make([][]float64, n)
for j := range n {
aCols[j] = make([]float64, n)
for i := range n {
aCols[j][i] = a.FloatAt(i*n + j)
}
}
if _, err := base.SolveSystem(name, freshRows(), aCols); err != nil {
return nil, nil, base.Errf("%s: %w", name, err)
}
// C = X·L⁻ᵀ, gathered by solving L·Z = Xᵀ and transposing.
cMat := make([]float64, n*n)
{
xT := make([][]float64, n)
for j := range n {
xT[j] = make([]float64, n)
for i := range n {
xT[j][i] = aCols[i][j] // column j of Xᵀ is row j of X
}
}
if _, err := base.SolveSystem(name, freshRows(), xT); err != nil {
return nil, nil, base.Errf("%s: %w", name, err)
}
for i := range n {
for j := range n {
cMat[i*n+j] = xT[j][i]
}
}
}
// Rounding leaves C a hair off symmetric; the eigensolver wants the
// exact form, so take the symmetric part.
for i := range n {
for j := i + 1; j < n; j++ {
m := (cMat[i*n+j] + cMat[j*n+i]) / 2
cMat[i*n+j] = m
cMat[j*n+i] = m
}
}
cArr := floatsToArray(cMat, []int{n, n})
values, yArr, err := Eigen(cArr)
if err != nil {
return nil, nil, base.Errf("%s: %w", name, err)
}
// V = L⁻ᵀ·Y: each eigenvector column solves Lᵀ·v = y.
ltBack := make([]float64, n*n)
lt := make([][]float64, n)
for i := range n {
lt[i] = ltBack[i*n : (i+1)*n]
for j := range n {
lt[i][j] = lFlat[j*n+i]
}
}
yCols := make([][]float64, n)
for j := range n {
yCols[j] = make([]float64, n)
for i := range n {
yCols[j][i] = yArr.FloatAt(i*n + j)
}
}
if _, err := base.SolveSystem(name, lt, yCols); err != nil {
return nil, nil, base.Errf("%s: %w", name, err)
}
vMat := make([]float64, n*n)
for j := range n {
for i := range n {
vMat[i*n+j] = yCols[j][i]
}
}
return values, floatsToArray(vMat, []int{n, n}), nil
}