1335 lines
40 KiB
Go
1335 lines
40 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package linalg
|
||
|
||
// SVD, Eigen, Pinverse, MatrixRank and Cond. Operates on
|
||
// float64 in and out; complex matrices are rejected.
|
||
//
|
||
// SVD: two-sided Jacobi rotation (Kogbetliantz / Press et al. NR3 §11.4).
|
||
// Both U and V are computed simultaneously by applying Jacobi
|
||
// rotations column-by-column on A and row-by-row on AᵀA; the
|
||
// algorithm is quadratic in the number of sweeps but converges
|
||
// robustly on ill-conditioned and rank-deficient matrices and is
|
||
// straightforward to keep correct. The matrix sizes tensor calls
|
||
// (n ≪ 256) keep this competitive with the QR approach.
|
||
//
|
||
// Eigen: implicit-shift QR on a symmetric tridiagonal matrix
|
||
// (Householder reduction first). Quadratic in sweeps but standard
|
||
// and stable. Both eigenvalues and eigenvectors are returned.
|
||
|
||
import (
|
||
"math"
|
||
"slices"
|
||
"sourcedock.dev/petrbalvin/tensor/internal/base"
|
||
"sourcedock.dev/petrbalvin/tensor/internal/core"
|
||
"sourcedock.dev/petrbalvin/tensor/internal/engine"
|
||
)
|
||
|
||
// svdMinWork is the measured parallel gate for one reflector
|
||
// application or matrix product slice, counted in element touches
|
||
// (items dispatched times the per-item inner length). Below it the
|
||
// work finishes before the worker dispatch pays off and runs on the
|
||
// calling goroutine. The bulge chase itself (symmetricQr,
|
||
// tridiagQrStep) stays serial: each rotation reads what the previous
|
||
// one wrote, and even the rounding-level entries far from the band
|
||
// feed a bulge a few rotations later, so no part of the tMat sweep may
|
||
// be reordered or narrowed. Only the eigenvector accumulation is
|
||
// order-safe: its mixes touch qMat alone, so they are recorded during
|
||
// the chase and replayed in batches over disjoint row ranges.
|
||
const svdMinWork = 16384
|
||
|
||
// svdMinItemsPerWorker is the floor on items per worker in the SVD
|
||
// dispatches.
|
||
const svdMinItemsPerWorker = 8
|
||
|
||
// qrFlushCap bounds the recorded rotation buffer between batched
|
||
// applications, keeping the replay scratch at a fixed size for any n.
|
||
const qrFlushCap = 65536
|
||
|
||
// qrRotation records one Givens rotation accumulated on the right of
|
||
// the eigenvector matrix: coordinates k and k+1 rotate by
|
||
// G = [[c, −s], [s, c]].
|
||
type qrRotation struct {
|
||
k int
|
||
c, s float64
|
||
}
|
||
|
||
// qrSweep collects the rotations of one symmetricQr run and applies
|
||
// them to the eigenvector accumulator in batches.
|
||
//
|
||
// Deferring the mixes is bit-identical to applying them inside the
|
||
// chase. A qMat cell (i, k) is touched only by rotations whose band
|
||
// position is k or k-1, each computing c·old + s·old' and -s·old +
|
||
// c·old' from the cell's current value and the rotation's (c, s); the
|
||
// pair (c, s) is read off tMat alone. Recording the rotations and
|
||
// replaying them in the same chronological order therefore feeds every
|
||
// cell exactly the arithmetic sequence the inline loop produced, and a
|
||
// flush only cuts that sequence into consecutive chunks. The replay
|
||
// walks disjoint row ranges per worker, so the parallel batch and the
|
||
// serial fallback write the same bits.
|
||
type qrSweep struct {
|
||
qMat []float64
|
||
n int
|
||
rots []qrRotation
|
||
// inline mirrors the pre-deferral behaviour: below qrDeferMin the
|
||
// chase applies each rotation to the accumulator as it runs,
|
||
// because the buffer, the flush and the worker dispatch cost more
|
||
// than the n-row mix they would save.
|
||
inline bool
|
||
}
|
||
|
||
// qrDeferMin is the accumulator size from which the eigenvector mixes
|
||
// defer to batched replay. Measured on the square sweep: at 128 the
|
||
// deferral loses a third of the run to the buffer and the dispatch,
|
||
// at 192 it measures even, and at 256 the batched replay wins 1.6
|
||
// times, with the gap widening as the mix grows cubically.
|
||
const qrDeferMin = 192
|
||
|
||
func newQrSweep(qMat []float64, n int) *qrSweep {
|
||
return &qrSweep{qMat: qMat, n: n, rots: make([]qrRotation, 0, 256),
|
||
inline: n < qrDeferMin}
|
||
}
|
||
|
||
// record applies one rotation inline below qrDeferMin and otherwise
|
||
// appends it, flushing the buffer at the cap so the scratch stays
|
||
// bounded for large n. Both modes feed the cell the same arithmetic in
|
||
// the same chronological position, so the result does not depend on
|
||
// the mode.
|
||
func (qa *qrSweep) record(k int, c, s float64) {
|
||
if qa.inline {
|
||
qMat, n := qa.qMat, qa.n
|
||
for i := range n {
|
||
qi := qMat[i*n+k]
|
||
qi1 := qMat[i*n+k+1]
|
||
qMat[i*n+k] = c*qi + s*qi1
|
||
qMat[i*n+k+1] = -s*qi + c*qi1
|
||
}
|
||
return
|
||
}
|
||
qa.rots = append(qa.rots, qrRotation{k: k, c: c, s: s})
|
||
if len(qa.rots) >= qrFlushCap {
|
||
qa.flush()
|
||
}
|
||
}
|
||
|
||
// flush applies the recorded rotations to qMat, one worker per row
|
||
// range, and empties the buffer. Below the dispatch gate the batch
|
||
// runs on the calling goroutine.
|
||
func (qa *qrSweep) flush() {
|
||
rots := qa.rots
|
||
if len(rots) == 0 {
|
||
return
|
||
}
|
||
n := qa.n
|
||
qMat := qa.qMat
|
||
apply := func(start, end int) {
|
||
for i := start; i < end; i++ {
|
||
row := i * n
|
||
for _, r := range rots {
|
||
qi := qMat[row+r.k]
|
||
qi1 := qMat[row+r.k+1]
|
||
qMat[row+r.k] = r.c*qi + r.s*qi1
|
||
qMat[row+r.k+1] = -r.s*qi + r.c*qi1
|
||
}
|
||
}
|
||
}
|
||
if len(rots)*n >= svdMinWork {
|
||
engine.ParallelMin(n, 1, func(start, end int) {
|
||
apply(start, end)
|
||
})
|
||
} else {
|
||
apply(0, n)
|
||
}
|
||
qa.rots = rots[:0]
|
||
}
|
||
|
||
// scaleWinLo and scaleWinHi bound the magnitude window the squared
|
||
// arithmetic of the decompositions is exact and finite in. Every square,
|
||
// sum of squares, product and fourth power of an entry inside it stays a
|
||
// normal float64: 2^200 squared is 2^400 and its fourth power 2^800, far
|
||
// inside the 1.8e308 ceiling, and 2^-300 squared is 2^-600, far above
|
||
// the 2^-1022 normal floor. Outside the window the raw accumulations
|
||
// underflow to zero, which silently skips reflectors, or overflow to
|
||
// +Inf, which zeroes a reflector's beta and poisons the update with NaN.
|
||
const (
|
||
scaleWinLo = -300 // 2^-300 ≈ 4.9e-91
|
||
scaleWinHi = 200 // 2^200 ≈ 1.6e60
|
||
)
|
||
|
||
// windowScale returns the exact power of two that moves maxAbs into
|
||
// [2^scaleWinLo, 2^scaleWinHi], and 1 when it already lies inside. A
|
||
// decomposition multiplies its matrix by the factor and its spectrum by
|
||
// 1/f: that is the same problem in scaled units, because eigenvalues and
|
||
// singular values scale with the matrix while eigenvectors and the
|
||
// orthogonal factors do not. Powers of two carry no rounding, so an
|
||
// in-window input keeps every bit of the original arithmetic and a
|
||
// rescaled one keeps the accuracy its exponent leaves it.
|
||
func windowScale(maxAbs float64) float64 {
|
||
if maxAbs <= 0 || math.IsNaN(maxAbs) || math.IsInf(maxAbs, 0) {
|
||
return 1
|
||
}
|
||
// Frexp splits maxAbs into m·2^e with m in [0.5, 1).
|
||
_, e := math.Frexp(maxAbs)
|
||
switch {
|
||
case e-1 > scaleWinHi:
|
||
return math.Ldexp(1, scaleWinHi-e)
|
||
case e-1 < scaleWinLo:
|
||
return math.Ldexp(1, scaleWinLo+1-e)
|
||
}
|
||
return 1
|
||
}
|
||
|
||
// maxMagF64 returns the largest absolute value in a real slice.
|
||
func maxMagF64(s []float64) float64 {
|
||
m := 0.0
|
||
for _, v := range s {
|
||
if a := math.Abs(v); a > m {
|
||
m = a
|
||
}
|
||
}
|
||
return m
|
||
}
|
||
|
||
// maxMagComplex returns the largest magnitude in a complex slice. The
|
||
// magnitudes come from math.Hypot, so an entry beyond ~1e154 keeps a
|
||
// finite magnitude where a sum of raw squares overflows.
|
||
func maxMagComplex(s []complex128) float64 {
|
||
m := 0.0
|
||
for _, z := range s {
|
||
if a := cmplxAbs(z); a > m {
|
||
m = a
|
||
}
|
||
}
|
||
return m
|
||
}
|
||
|
||
// scaleFloats multiplies every entry of s by f; f is an exact power of
|
||
// two, so the entries come back bit for bit from unscaleFloats.
|
||
func scaleFloats(s []float64, f float64) {
|
||
for i := range s {
|
||
s[i] *= f
|
||
}
|
||
}
|
||
|
||
// unscaleFloats divides every entry of s by f, an exact power of two.
|
||
func unscaleFloats(s []float64, f float64) {
|
||
for i := range s {
|
||
s[i] /= f
|
||
}
|
||
}
|
||
|
||
// scaleComplexes multiplies every entry of s by the real power of two f.
|
||
func scaleComplexes(s []complex128, f float64) {
|
||
for i := range s {
|
||
s[i] = complex(real(s[i])*f, imag(s[i])*f)
|
||
}
|
||
}
|
||
|
||
// unscaleComplexes divides every entry of s by the real power of two f.
|
||
func unscaleComplexes(s []complex128, f float64) {
|
||
for i := range s {
|
||
s[i] = complex(real(s[i])/f, imag(s[i])/f)
|
||
}
|
||
}
|
||
|
||
// SVD returns the thin singular value decomposition a = U · Σ · Vᵀ
|
||
// of an m×n matrix a with m ≥ n. For m < n the matrix is
|
||
// transposed first and the orthogonal factors are swapped on
|
||
// return. U is m×n with orthonormal columns (Uᵀ U = I_n); Σ is a
|
||
// 1-D length-n array of non-negative singular values sorted
|
||
// descending; Vᵀ is n×n orthogonal.
|
||
//
|
||
// Algorithm:
|
||
// 1. Bidiagonalise A, giving A = U₁ · B · V₁ᵀ via Householder.
|
||
// 2. Form C = Bᵀ B (n × n symmetric tridiagonal).
|
||
// 3. Symmetric implicit-shift QR with Wilkinson shift on C,
|
||
// accumulating rotations into V = V₁ · Q.
|
||
// 4. Σ = √(eigenvalues of C). Reconstruct U = A · V / Σ and
|
||
// orthogonalise via Gram-Schmidt to absorb numerical drift.
|
||
//
|
||
// This is the standard double-step SVD via Bᵀ B + symmetric QR:
|
||
// robust, well-understood, and reuses the symmetric tridiagonal
|
||
// eigensolver already shipped for `Eigen`.
|
||
//
|
||
// The double step prices the small end of the spectrum: the singular
|
||
// values come from the eigenvalues of BᵀB, so their accuracy on very
|
||
// small singular values is that of a squared condition number, fine
|
||
// for rank decisions, weaker for resolving near-null directions.
|
||
// `SVDComplex` states the same contract for its Aᴴ·A route.
|
||
func SVD(a *core.Array) (u, sigma, vt *core.Array, err error) {
|
||
if a.Dtype() == core.Complex {
|
||
return nil, nil, nil, base.Errf("SVD: complex matrices are not supported")
|
||
}
|
||
if a.NDim() != 2 {
|
||
return nil, nil, nil, base.Errf("SVD: needs a 2-D matrix, got shape %s", base.ShapeText(a.Shape()))
|
||
}
|
||
m, n := a.Shape()[0], a.Shape()[1]
|
||
if m == 0 || n == 0 {
|
||
return nil, nil, nil, base.Errf("SVD: zero-sized matrix, got shape %s", base.ShapeText(a.Shape()))
|
||
}
|
||
transposed := false
|
||
Amat := denseFloats(a, a.Shape()[0], a.Shape()[1])
|
||
// Outside the safe window every squared intermediate of the
|
||
// bidiagonalisation, of C = BᵀB and of the σ recovery would leave
|
||
// the normal range. The matrix is moved into the window for the
|
||
// computation and the singular values, which carry the scale, are
|
||
// moved back once the factorisation is done; the orthogonal factors
|
||
// are scale-free.
|
||
ws := windowScale(maxMagF64(Amat))
|
||
if ws != 1 {
|
||
scaleFloats(Amat, ws)
|
||
}
|
||
origM, origN := m, n
|
||
if m < n {
|
||
transposed = true
|
||
Amat = base.TransposeFlat(Amat, m, n)
|
||
m, n = n, m
|
||
}
|
||
bDiag, bSuper, v1 := bidiagonalise(Amat, m, n)
|
||
// Form C = Bᵀ B (n × n symmetric tridiagonal): AᵀA = V₁ C V₁ᵀ.
|
||
// The diagonal carries the superdiagonal's contribution:
|
||
// C[i,i] = B[i,i]² + B[i−1,i]²; the off-diagonal is
|
||
// C[i,i+1] = B[i,i]·B[i,i+1].
|
||
cMat := make([]float64, n*n)
|
||
for i := 0; i < n; i++ {
|
||
cMat[i*n+i] = bDiag[i] * bDiag[i]
|
||
if i > 0 {
|
||
cMat[i*n+i] += bSuper[i-1] * bSuper[i-1]
|
||
}
|
||
if i+1 < n {
|
||
cMat[i*n+i+1] = bDiag[i] * bSuper[i]
|
||
cMat[(i+1)*n+i] = cMat[i*n+i+1]
|
||
}
|
||
}
|
||
// Eigendecompose C with the shipped symmetric solver: C = W Λ Wᵀ,
|
||
// so the right singular vectors are V = V₁·W with Σ = √Λ. C is
|
||
// (n × n) symmetric and tiny (n ≪ 1024 in the typical workspace),
|
||
// so the eigensolver is much cheaper than a QR sweep per call.
|
||
// C is exactly symmetric by construction, so the internals of
|
||
// `Eigen` run directly.
|
||
tMat, qMat := householderTridiag(cMat, n)
|
||
qT := base.TransposeFlat(qMat, n, n)
|
||
if err := symmetricQr(tMat, qT, n); err != nil {
|
||
return nil, nil, nil, base.Errf("SVD: %w", err)
|
||
}
|
||
eVals := make([]float64, n)
|
||
for i := range n {
|
||
eVals[i] = tMat[i*n+i]
|
||
}
|
||
eIdx := sortAscIndices(eVals)
|
||
cVals := make([]float64, n) // ascending eigenvalues, as Eigen returns them
|
||
eVecs := make([]float64, n*n)
|
||
for j := range n {
|
||
cVals[j] = eVals[eIdx[j]]
|
||
for i := range n {
|
||
eVecs[i*n+j] = qT[i*n+eIdx[j]]
|
||
}
|
||
}
|
||
vMat := make([]float64, n*n) // V = V₁·W
|
||
// Rows are independent; each element keeps its serial dot over k.
|
||
if n*n*n >= svdMinWork {
|
||
engine.ParallelMin(n, 1, func(start, end int) {
|
||
for i := start; i < end; i++ {
|
||
for j := range n {
|
||
s := 0.0
|
||
for k := range n {
|
||
s += v1[i*n+k] * eVecs[k*n+j]
|
||
}
|
||
vMat[i*n+j] = s
|
||
}
|
||
}
|
||
})
|
||
} else {
|
||
for i := range n {
|
||
for j := range n {
|
||
s := 0.0
|
||
for k := range n {
|
||
s += v1[i*n+k] * eVecs[k*n+j]
|
||
}
|
||
vMat[i*n+j] = s
|
||
}
|
||
}
|
||
}
|
||
// Eigen returns ascending order; the SVD contract wants Σ
|
||
// descending, so permute the columns of V to match.
|
||
idx := sortDescIndices(cVals)
|
||
permuteCols(vMat, n, n, idx)
|
||
// Recover Σ directly from A as σₖ = ‖A·vₖ‖: the BᵀB round trip
|
||
// squares the condition number, so reading √λ back would lose half
|
||
// the digits on ill-conditioned inputs. Columns are independent and
|
||
// each norm keeps its serial accumulation order over i, then j.
|
||
sorted := make([]float64, n)
|
||
if m*n*n >= svdMinWork {
|
||
engine.ParallelMin(n, 1, func(start, end int) {
|
||
for k := start; k < end; k++ {
|
||
norm := 0.0
|
||
for i := range m {
|
||
acc := 0.0
|
||
for j := range n {
|
||
acc += Amat[i*n+j] * vMat[j*n+k]
|
||
}
|
||
norm += acc * acc
|
||
}
|
||
sorted[k] = math.Sqrt(norm)
|
||
}
|
||
})
|
||
} else {
|
||
for k := range n {
|
||
norm := 0.0
|
||
for i := range m {
|
||
acc := 0.0
|
||
for j := range n {
|
||
acc += Amat[i*n+j] * vMat[j*n+k]
|
||
}
|
||
norm += acc * acc
|
||
}
|
||
sorted[k] = math.Sqrt(norm)
|
||
}
|
||
}
|
||
// The recovered Σ can disagree with the eigenvalue order by
|
||
// rounding on near-degenerate values; settle on one descending
|
||
// order before building U, permuting V along with it.
|
||
if ord := sortDescIndices(sorted); !isIdentityPerm(ord) {
|
||
permuteCols(vMat, n, n, ord)
|
||
perm := make([]float64, n)
|
||
copy(perm, sorted)
|
||
for i := range n {
|
||
sorted[i] = perm[ord[i]]
|
||
}
|
||
}
|
||
// Thin U = A·V·Σ⁻¹ from the working (m ≥ n) matrix, with A and Σ in
|
||
// the same scaled units.
|
||
uThin := reconstructUThin(Amat, vMat, sorted, m, n)
|
||
// The singular values carry the matrix's scale and the orthogonal
|
||
// factors do not, so only Σ is moved back out of the scaled units
|
||
// the factorisation ran in.
|
||
if ws != 1 {
|
||
unscaleFloats(sorted, ws)
|
||
}
|
||
U := floatsToArray(uThin, []int{m, n})
|
||
S := floatsToArray(sorted, []int{n})
|
||
Vt := floatsToArray(base.TransposeFlat(vMat, n, n), []int{n, n})
|
||
if transposed {
|
||
// The input had m < n; the factorisation ran on the transpose
|
||
// Aᵀ = U' Σ V'ᵀ, so the original reads A = V' Σ U'ᵀ:
|
||
// U_orig = V' itself (origM × origM) and Vᵀ_orig = (thin U')ᵀ.
|
||
return floatsToArray(vMat, []int{origM, origM}),
|
||
S,
|
||
floatsToArray(base.TransposeFlat(uThin, m, n), []int{origM, origN}),
|
||
nil
|
||
}
|
||
return U, S, Vt, nil
|
||
}
|
||
|
||
// bidiagonalise reduces A (m×n with m ≥ n, row-major) to upper
|
||
// bidiagonal form via Golub-Kahan Householder reflectors: B =
|
||
// H_p…H₁·A·K₁…K_q, so A = U₁·B·V₁ᵀ with V₁ = K₁K₂…K_q accumulated by
|
||
// right-multiplying the reflectors into its columns. The left factor U₁
|
||
// is not accumulated: the only caller recovers U from A·V after the
|
||
// sweep, and carrying the m×m accumulator cost an eye(m) matrix and an
|
||
// m-row update per left reflector without feeding any returned value.
|
||
// The bidiagonal and the right accumulator are exactly the values the
|
||
// accumulation carried: neither reads uMat.
|
||
//
|
||
// Reflectors apply strictly in sweep order; within one reflector every
|
||
// touched column or row is updated exactly once from the frozen
|
||
// reflector vector and beta, so the per-line work (dot along the line,
|
||
// then the same-operand rank-1 subtraction) dispatches over the lines
|
||
// of bFull and the accumulator in one parallel range. The per-element
|
||
// sequence is the serial one, so the factorisation is bit-identical
|
||
// across crew sizes.
|
||
func bidiagonalise(aMat []float64, m, n int) (bDiag, bSuper []float64, vMat []float64) {
|
||
bDiag = make([]float64, n)
|
||
if n > 1 {
|
||
bSuper = make([]float64, n-1)
|
||
}
|
||
bFull := make([]float64, m*n)
|
||
copy(bFull, aMat)
|
||
vMat = eye(n)
|
||
// Reflector scratch reused across the sweep; each step uses the
|
||
// prefix it needs.
|
||
vec := make([]float64, m)
|
||
vecR := make([]float64, n)
|
||
for k := range n {
|
||
// Left reflector: rows k..m-1 of column k, zeroing below (k, k).
|
||
lv := vec[:m-k]
|
||
for i := range lv {
|
||
lv[i] = bFull[(k+i)*n+k]
|
||
}
|
||
hh := householderVectorInto(lv, lv)
|
||
if hh.beta != 0 {
|
||
ln := m - k
|
||
bColumn := func(j int) {
|
||
dot := 0.0
|
||
for i := range ln {
|
||
dot += hh.v[i] * bFull[(k+i)*n+j]
|
||
}
|
||
w := hh.beta * dot
|
||
for i := range ln {
|
||
bFull[(k+i)*n+j] -= hh.v[i] * w
|
||
}
|
||
}
|
||
if (n-k)*ln >= svdMinWork {
|
||
engine.ParallelMin(n-k, svdMinItemsPerWorker, func(start, end int) {
|
||
for j := start; j < end; j++ {
|
||
bColumn(k + j)
|
||
}
|
||
})
|
||
} else {
|
||
for j := k; j < n; j++ {
|
||
bColumn(j)
|
||
}
|
||
}
|
||
}
|
||
if k+1 >= n {
|
||
break
|
||
}
|
||
// Right reflector: columns k+1..n-1 of row k, zeroing right of
|
||
// (k, k+1).
|
||
rv := vecR[:n-k-1]
|
||
for j := range rv {
|
||
rv[j] = bFull[k*n+(k+1+j)]
|
||
}
|
||
hhR := householderVectorInto(rv, rv)
|
||
if hhR.beta != 0 {
|
||
ln := len(rv)
|
||
bRow := func(i int) {
|
||
dot := 0.0
|
||
for j := range ln {
|
||
dot += hhR.v[j] * bFull[i*n+(k+1+j)]
|
||
}
|
||
w := hhR.beta * dot
|
||
for j := range ln {
|
||
bFull[i*n+(k+1+j)] -= hhR.v[j] * w
|
||
}
|
||
}
|
||
vRow := func(j int) {
|
||
dot := 0.0
|
||
for i := range ln {
|
||
dot += hhR.v[i] * vMat[j*n+(k+1+i)]
|
||
}
|
||
w := hhR.beta * dot
|
||
for i := range ln {
|
||
vMat[j*n+(k+1+i)] -= hhR.v[i] * w
|
||
}
|
||
}
|
||
// V₁ = K₁K₂…: likewise from the right, columns k+1..n-1.
|
||
// Rows k..m-1 of bFull and rows of vMat: disjoint lines.
|
||
items := (m - k) + n
|
||
if items*ln >= svdMinWork {
|
||
engine.ParallelMin(items, svdMinItemsPerWorker, func(start, end int) {
|
||
for it := start; it < end; it++ {
|
||
if it < m-k {
|
||
bRow(k + it)
|
||
} else {
|
||
vRow(it - (m - k))
|
||
}
|
||
}
|
||
})
|
||
} else {
|
||
for i := k; i < m; i++ {
|
||
bRow(i)
|
||
}
|
||
for j := range n {
|
||
vRow(j)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for i := range n {
|
||
bDiag[i] = bFull[i*n+i]
|
||
if i+1 < n {
|
||
bSuper[i] = bFull[i*n+i+1]
|
||
}
|
||
}
|
||
return bDiag, bSuper, vMat
|
||
}
|
||
|
||
// smallOff is the deflation threshold for the tridiagonal QR sweep.
|
||
func smallOff(d1, d2 float64) float64 {
|
||
return base.EpsF * (math.Abs(d1) + math.Abs(d2))
|
||
}
|
||
|
||
// reconstructUThin builds thin U = A_orig · V · diag(1/Σ) when Σ is
|
||
// non-zero, and falls back to the kernel projection for small
|
||
// σᵢ. aOrig is (post-transpose) input shape (m, n). vMat is (n, n).
|
||
// sVals are the singular values in matched column order. Result
|
||
// has shape (m, n).
|
||
//
|
||
// The candidate projection A_orig·V is computed once into a scratch,
|
||
// dispatched over the rows of aOrig: every element is the same serial
|
||
// dot over j (ascending) the per-column loop used to run, so the
|
||
// values are bit-identical and only their computing order moved. The
|
||
// modified Gram-Schmidt pass stays serial: column k reads every
|
||
// earlier column, and each projection's dot is an order-bound
|
||
// reduction over the column being updated.
|
||
func reconstructUThin(aOrig, vMat, sVals []float64, m, n int) []float64 {
|
||
biggest := 0.0
|
||
for _, v := range sVals {
|
||
if v > biggest {
|
||
biggest = v
|
||
}
|
||
}
|
||
thresh := base.EpsF * float64(m) * biggest
|
||
// The Gram-Schmidt pass tests the norm of a column that the division
|
||
// by σ makes unit-length, so its floor is an absolute one relative to
|
||
// that unit and not to the largest singular value: with the singular
|
||
// values themselves as the reference, a matrix whose spectrum is
|
||
// above ~1e16 would route every healthy column to the kernel rebuild
|
||
// below and lose the factorisation.
|
||
unitTol := base.EpsF * float64(m)
|
||
// The un-orthonormalised candidate is A_orig·V/Σ on the columns
|
||
// where σ is non-tiny (and A_orig·V otherwise), re-orthonormalised
|
||
// via modified Gram-Schmidt below so that UᵀU = I_n holds even when
|
||
// the reduction leaves small numerical drift. A column whose
|
||
// residual is essentially zero (σ at rounding level) is rebuilt from
|
||
// the best orthogonalised coordinate candidate e_j: the largest
|
||
// residual is at least 1/√m, because a unit vector of the
|
||
// orthogonal complement has a coordinate at least that large.
|
||
uMat := make([]float64, m*n)
|
||
// Scratch reused across columns: the working column and, for kernel
|
||
// columns, the candidate buffers (fully overwritten before use).
|
||
best := make([]float64, m)
|
||
cand := make([]float64, m)
|
||
// proj holds the un-orthonormalised projection A_orig·V, one value
|
||
// per (row, singular vector) pair, each from the same serial dot.
|
||
proj := engine.GetFloat64Buf(m * n)
|
||
defer engine.PutFloat64Buf(proj)
|
||
if m*n*n >= svdMinWork {
|
||
engine.ParallelMin(m, 1, func(start, end int) {
|
||
for i := start; i < end; i++ {
|
||
row := aOrig[i*n : i*n+n]
|
||
out := i * n
|
||
for k := range n {
|
||
s := 0.0
|
||
for j := range n {
|
||
s += row[j] * vMat[j*n+k]
|
||
}
|
||
proj[out+k] = s
|
||
}
|
||
}
|
||
})
|
||
} else {
|
||
for i := range m {
|
||
for k := range n {
|
||
s := 0.0
|
||
for j := range n {
|
||
s += aOrig[i*n+j] * vMat[j*n+k]
|
||
}
|
||
proj[i*n+k] = s
|
||
}
|
||
}
|
||
}
|
||
for k := range n {
|
||
if sVals[k] > thresh {
|
||
inv := 1 / sVals[k]
|
||
for i := range m {
|
||
uMat[i*n+k] = proj[i*n+k] * inv
|
||
}
|
||
} else {
|
||
for i := range m {
|
||
uMat[i*n+k] = proj[i*n+k]
|
||
}
|
||
}
|
||
// Subtract projections on the earlier columns.
|
||
for j := range k {
|
||
dot := 0.0
|
||
for i := range m {
|
||
dot += uMat[i*n+j] * uMat[i*n+k]
|
||
}
|
||
for i := range m {
|
||
uMat[i*n+k] -= dot * uMat[i*n+j]
|
||
}
|
||
}
|
||
nrm := 0.0
|
||
for i := range m {
|
||
nrm += uMat[i*n+k] * uMat[i*n+k]
|
||
}
|
||
nrm = math.Sqrt(nrm)
|
||
if nrm > unitTol {
|
||
inv := 1 / nrm
|
||
for i := range m {
|
||
uMat[i*n+k] *= inv
|
||
}
|
||
continue
|
||
}
|
||
// Kernel column: orthogonalise every coordinate candidate and
|
||
// keep the healthiest residual.
|
||
bestRes := -1.0
|
||
for j := range m {
|
||
for i := range m {
|
||
cand[i] = 0
|
||
}
|
||
cand[j] = 1
|
||
for range 2 {
|
||
for j2 := range k {
|
||
d := 0.0
|
||
for i := range m {
|
||
d += uMat[i*n+j2] * cand[i]
|
||
}
|
||
for i := range m {
|
||
cand[i] -= d * uMat[i*n+j2]
|
||
}
|
||
}
|
||
}
|
||
rn := 0.0
|
||
for i := range m {
|
||
rn += cand[i] * cand[i]
|
||
}
|
||
if rn > bestRes {
|
||
bestRes = rn
|
||
copy(best, cand)
|
||
}
|
||
}
|
||
if bestRes > 0 {
|
||
inv := 1 / math.Sqrt(bestRes)
|
||
for i := range m {
|
||
uMat[i*n+k] = best[i] * inv
|
||
}
|
||
}
|
||
}
|
||
return uMat
|
||
}
|
||
|
||
// Eigen returns the eigenvalues and orthonormal eigenvectors of a
|
||
// real symmetric n×n matrix a. Eigenvalues are returned in a 1-D
|
||
// float array, sorted in ascending order; the eigenvectors are
|
||
// the columns of an n×n orthogonal array. Asymmetric matrices are
|
||
// not supported.
|
||
func Eigen(a *core.Array) (values, vectors *core.Array, err error) {
|
||
if a.Dtype() == core.Complex {
|
||
return nil, nil, base.Errf("Eigen: complex matrices are not supported")
|
||
}
|
||
if a.NDim() != 2 || a.Shape()[0] != a.Shape()[1] {
|
||
return nil, nil, base.Errf("Eigen: needs a square 2-D matrix, got shape %s", base.ShapeText(a.Shape()))
|
||
}
|
||
n := a.Shape()[0]
|
||
mat := denseFloats(a, n, n)
|
||
// A matrix outside the safe window would drive the reflector norms
|
||
// and the sweep's squared accumulation past the representable range;
|
||
// the tridiagonalisation and the QR sweep run on it scaled into the
|
||
// window and the eigenvalues are scaled back. The eigenvectors are
|
||
// the same for every positive multiple of the matrix.
|
||
ws := windowScale(maxMagF64(mat))
|
||
if ws != 1 {
|
||
scaleFloats(mat, ws)
|
||
}
|
||
if !isSymmetric(mat, n) {
|
||
return nil, nil, base.Errf("Eigen: matrix is not symmetric within 1e-12 tolerance")
|
||
}
|
||
tMat, qMat := householderTridiag(mat, n)
|
||
// The reduction maintains T = qMat·A·qMatᵀ (reflectors applied on
|
||
// the left of qMat), so the eigenvector matrix of A after the QR
|
||
// sweep is qMatᵀ·G_total. The sweep accumulates rotations on the
|
||
// right of its accumulator, hence the transpose before the call.
|
||
qT := base.TransposeFlat(qMat, n, n)
|
||
if err := symmetricQr(tMat, qT, n); err != nil {
|
||
return nil, nil, base.Errf("Eigen: %w", err)
|
||
}
|
||
vals := make([]float64, n)
|
||
for i := range n {
|
||
vals[i] = tMat[i*n+i]
|
||
}
|
||
idx := sortAscIndices(vals)
|
||
out := make([]float64, n)
|
||
for i := range n {
|
||
out[i] = vals[idx[i]]
|
||
}
|
||
sortedCols := make([]float64, n*n)
|
||
for j := range n {
|
||
for i := range n {
|
||
sortedCols[i*n+j] = qT[i*n+idx[j]]
|
||
}
|
||
}
|
||
if ws != 1 {
|
||
unscaleFloats(out, ws)
|
||
}
|
||
return floatsToArray(out, []int{n}), floatsToArray(sortedCols, []int{n, n}), nil
|
||
}
|
||
|
||
// Pinverse returns the Moore-Penrose pseudoinverse of a 2-D matrix,
|
||
// built from the SVD by inverting singular values strictly greater
|
||
// than ε. When eps ≤ 0 the default is max(m, n) · max(Σ) · base.EpsF.
|
||
func Pinverse(a *core.Array, eps float64) (*core.Array, error) {
|
||
if a.Dtype() == core.Complex {
|
||
return nil, base.Errf("Pinverse: complex matrices are not supported")
|
||
}
|
||
if a.NDim() != 2 {
|
||
return nil, base.Errf("Pinverse: needs a 2-D matrix, got shape %s", base.ShapeText(a.Shape()))
|
||
}
|
||
m, n := a.Shape()[0], a.Shape()[1]
|
||
if m == 0 || n == 0 {
|
||
return nil, base.Errf("Pinverse: zero-sized matrix, got shape %s", base.ShapeText(a.Shape()))
|
||
}
|
||
u, sigma, vt, err := SVD(a)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
sVals := sigma.RawFloats()
|
||
// The thin factorisation carries min(m, n) singular values: U is
|
||
// (m, r) and Vᵀ is (r, n), whatever the input aspect ratio.
|
||
r := min(m, n)
|
||
maxS := 0.0
|
||
for _, s := range sVals {
|
||
if s > maxS {
|
||
maxS = s
|
||
}
|
||
}
|
||
if eps <= 0 {
|
||
dim := max(n, m)
|
||
eps = float64(dim) * maxS * base.EpsF
|
||
}
|
||
dInv := make([]float64, r)
|
||
for i, s := range sVals {
|
||
if s > eps {
|
||
dInv[i] = 1 / s
|
||
}
|
||
}
|
||
uMat := denseFloats(u, m, r)
|
||
vtMat := denseFloats(vt, r, n)
|
||
out := make([]float64, n*m)
|
||
// A⁺ = V · Σ⁻¹ · Uᵀ = (V · Σ⁻¹) · Uᵀ, shape (n, m):
|
||
// out[i,j] = Σ_k V[i,k] · Σ⁻¹[k] · U[j,k], with V[i,k] = Vᵀ[k,i].
|
||
for i := range n {
|
||
for j := range m {
|
||
s := 0.0
|
||
for k := range r {
|
||
s += vtMat[k*n+i] * dInv[k] * uMat[j*r+k]
|
||
}
|
||
out[i*m+j] = s
|
||
}
|
||
}
|
||
return floatsToArray(out, []int{n, m}), nil
|
||
}
|
||
|
||
// MatrixRank returns the number of singular values of a 2-D matrix
|
||
// strictly greater than eps. With eps ≤ 0 the default is
|
||
// max(m, n) · max(Σ) · base.EpsF.
|
||
func MatrixRank(a *core.Array, eps float64) (int, error) {
|
||
if a.Dtype() == core.Complex {
|
||
return 0, base.Errf("MatrixRank: complex matrices are not supported")
|
||
}
|
||
if a.NDim() != 2 {
|
||
return 0, base.Errf("MatrixRank: needs a 2-D matrix, got shape %s", base.ShapeText(a.Shape()))
|
||
}
|
||
_, sigma, _, err := SVD(a)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return countAboveThreshold(sigma.RawFloats(), eps, a.Shape()[0], a.Shape()[1]), nil
|
||
}
|
||
|
||
// Cond returns the 2-norm condition number σ_max / σ_min of a 2-D
|
||
// matrix. If any singular value is zero (or at-or-below a positive
|
||
// eps) the result is +Inf, mirroring the standard convention.
|
||
func Cond(a *core.Array, eps float64) (float64, error) {
|
||
if a.Dtype() == core.Complex {
|
||
return 0, base.Errf("Cond: complex matrices are not supported")
|
||
}
|
||
if a.NDim() != 2 {
|
||
return 0, base.Errf("Cond: needs a 2-D matrix, got shape %s", base.ShapeText(a.Shape()))
|
||
}
|
||
_, sigma, _, err := SVD(a)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return condValue(sigma.RawFloats(), eps, a.Shape()[0], a.Shape()[1]), nil
|
||
}
|
||
|
||
// householderTridiag reduces a symmetric matrix to tridiagonal form via
|
||
// Householder reflectors, accumulating the orthogonal factor into qMat.
|
||
//
|
||
// The sweep order is untouched. Within one reflector the LEFT
|
||
// application to T (columns) and the accumulation into qMat (columns)
|
||
// run over disjoint buffers, so they share one dispatch; the RIGHT
|
||
// application to T (rows) must follow, because it reads the entries the
|
||
// left application has just written; the drift symmetrisation follows
|
||
// it serially. Each line is updated exactly once from the frozen
|
||
// reflector with the serial dot order, so the result is bit-identical.
|
||
func householderTridiag(aMat []float64, n int) (tMat, qMat []float64) {
|
||
tMat = make([]float64, n*n)
|
||
copy(tMat, aMat)
|
||
qMat = eye(n)
|
||
// Reflector scratch reused across the sweep.
|
||
v := make([]float64, n)
|
||
for k := 0; k < n-2; k++ {
|
||
vv := v[:n-k-1]
|
||
for i := 0; i < n-k-1; i++ {
|
||
vv[i] = tMat[(k+1+i)*n+k]
|
||
}
|
||
u := householderVectorInto(vv, vv)
|
||
if u.beta == 0 {
|
||
continue
|
||
}
|
||
ln := n - k - 1
|
||
tColumn := func(j int) {
|
||
dot := 0.0
|
||
for i := range ln {
|
||
dot += u.v[i] * tMat[(k+1+i)*n+j]
|
||
}
|
||
w := u.beta * dot
|
||
for i := range ln {
|
||
tMat[(k+1+i)*n+j] -= u.v[i] * w
|
||
}
|
||
}
|
||
qColumn := func(j int) {
|
||
dot := 0.0
|
||
for i := range ln {
|
||
dot += u.v[i] * qMat[(k+1+i)*n+j]
|
||
}
|
||
w := u.beta * dot
|
||
for i := range ln {
|
||
qMat[(k+1+i)*n+j] -= u.v[i] * w
|
||
}
|
||
}
|
||
// Apply H from both sides to T: left pass over the columns of T
|
||
// together with the qMat accumulation (disjoint buffers).
|
||
items := (n - k) + n
|
||
if items*ln >= svdMinWork {
|
||
engine.ParallelMin(items, svdMinItemsPerWorker, func(start, end int) {
|
||
for it := start; it < end; it++ {
|
||
if it < n-k {
|
||
tColumn(k + it)
|
||
} else {
|
||
qColumn(it - (n - k))
|
||
}
|
||
}
|
||
})
|
||
} else {
|
||
for j := k; j < n; j++ {
|
||
tColumn(j)
|
||
}
|
||
for j := range n {
|
||
qColumn(j)
|
||
}
|
||
}
|
||
// Right pass over the rows of T: reads the left pass's writes,
|
||
// so it stays strictly after it.
|
||
tRow := func(i int) {
|
||
dot := 0.0
|
||
for j := range ln {
|
||
dot += u.v[j] * tMat[i*n+(k+1)+j]
|
||
}
|
||
w := u.beta * dot
|
||
for j := range ln {
|
||
tMat[i*n+(k+1)+j] -= u.v[j] * w
|
||
}
|
||
}
|
||
if (n-k)*ln >= svdMinWork {
|
||
engine.ParallelMin(n-k, svdMinItemsPerWorker, func(start, end int) {
|
||
for i := start; i < end; i++ {
|
||
tRow(k + i)
|
||
}
|
||
})
|
||
} else {
|
||
for i := k; i < n; i++ {
|
||
tRow(i)
|
||
}
|
||
}
|
||
// Symmetrise against numerical drift.
|
||
for i := k + 1; i < n; i++ {
|
||
for j := i + 1; j < n; j++ {
|
||
avg := (tMat[i*n+j] + tMat[j*n+i]) / 2
|
||
tMat[i*n+j] = avg
|
||
tMat[j*n+i] = avg
|
||
}
|
||
}
|
||
}
|
||
return tMat, qMat
|
||
}
|
||
|
||
// symmetricQr runs the implicit-shift symmetric QR algorithm with
|
||
// the Wilkinson shift on a symmetric tridiagonal tMat. Returns the
|
||
// updated tridiagonal (whose diagonal is the eigenvalues) and the
|
||
// eigenvector matrix Q, or an error when the sweep exhausts its
|
||
// passes without deflating every subdiagonal entry: an unconverged
|
||
// spectrum is never returned as an answer.
|
||
func symmetricQr(tMat, qMat []float64, n int) error {
|
||
maxIters := max(30*n, 30)
|
||
// Deflation floor relative to the matrix norm: a purely
|
||
// neighbour-relative threshold never triggers when both diagonal
|
||
// entries are small, even though the off-diagonal is already at
|
||
// rounding level of the whole transform (backward stability
|
||
// guarantees nothing better than eps·‖T‖ anyway).
|
||
scale := 0.0
|
||
for i := range n {
|
||
scale += tMat[i*n+i] * tMat[i*n+i]
|
||
if i+1 < n {
|
||
e := tMat[i*n+i+1]
|
||
scale += 2 * e * e
|
||
}
|
||
}
|
||
tolAbs := base.EpsF * math.Sqrt(scale)
|
||
negligible := func(i int) bool { // tests the subdiagonal entry (i, i-1)
|
||
e := math.Abs(tMat[i*n+(i-1)])
|
||
// An exactly zero entry is deflated by definition: a zero
|
||
// matrix has tolAbs 0 and relative floors of 0, and a strict
|
||
// inequality would never deflate it.
|
||
return e == 0 || e < smallOff(tMat[i*n+i], tMat[(i-1)*n+(i-1)]) || e < tolAbs
|
||
}
|
||
acc := newQrSweep(qMat, n)
|
||
for range maxIters {
|
||
h := n - 1
|
||
for h > 0 && negligible(h) {
|
||
tMat[h*n+(h-1)] = 0
|
||
tMat[(h-1)*n+h] = 0
|
||
h--
|
||
}
|
||
if h <= 0 {
|
||
break
|
||
}
|
||
// The active block containing h reaches up to the first
|
||
// negligible subdiagonal entry (its decoupling boundary):
|
||
// expanding l past that boundary would leave a zero entry
|
||
// inside the window, where the bulge chase would abort and
|
||
// strand the block below it.
|
||
l := h - 1
|
||
for l > 0 && !negligible(l) {
|
||
l--
|
||
}
|
||
if l > 0 {
|
||
tMat[l*n+(l-1)] = 0
|
||
tMat[(l-1)*n+l] = 0
|
||
}
|
||
if h <= l {
|
||
continue
|
||
}
|
||
if h-l == 1 {
|
||
// A 2×2 block is diagonalised exactly by one Jacobi
|
||
// rotation; the shift chase leaves a rounding-level
|
||
// off-diagonal that can sit forever above the deflation
|
||
// threshold, so finish it off directly.
|
||
jacobi2x2(tMat, acc, l, n)
|
||
tMat[l*n+h] = 0
|
||
tMat[h*n+l] = 0
|
||
continue
|
||
}
|
||
d := tMat[(h-1)*n+(h-1)]
|
||
e := tMat[(h-1)*n+h]
|
||
f := tMat[h*n+h]
|
||
shift := wilkinsonShift2x2(d, e, f)
|
||
tridiagQrStep(tMat, acc, l, h, n, shift)
|
||
}
|
||
// An exhausted sweep must not pass its tridiagonal off as a
|
||
// spectrum: every sibling iterator here (hessenbergQr, the
|
||
// Golub-Reinsch SVD, schurQR) errors on exhaustion, and so does
|
||
// this one.
|
||
for i := 1; i < n; i++ {
|
||
if !negligible(i) {
|
||
return base.Errf("the QR sweep failed to deflate a %d×%d matrix in %d passes", n, n, maxIters)
|
||
}
|
||
}
|
||
acc.flush()
|
||
return nil
|
||
}
|
||
|
||
// applyGivens applies the rotation G = [[c, −s], [s, c]] on coordinates
|
||
// (k, k+1) as a similarity transform of tMat and records it for the
|
||
// batched accumulation on the right of the eigenvector matrix.
|
||
func applyGivens(tMat []float64, acc *qrSweep, k, n int, c, s float64) {
|
||
for j := range n {
|
||
tk := tMat[k*n+j]
|
||
tk1 := tMat[(k+1)*n+j]
|
||
tMat[k*n+j] = c*tk + s*tk1
|
||
tMat[(k+1)*n+j] = -s*tk + c*tk1
|
||
}
|
||
for i := range n {
|
||
ti := tMat[i*n+k]
|
||
ti1 := tMat[i*n+k+1]
|
||
tMat[i*n+k] = c*ti + s*ti1
|
||
tMat[i*n+k+1] = -s*ti + c*ti1
|
||
}
|
||
acc.record(k, c, s)
|
||
}
|
||
|
||
// jacobi2x2 diagonalises the symmetric 2×2 block at (k, k+1) exactly
|
||
// with one Jacobi rotation, recording the rotation into acc.
|
||
func jacobi2x2(tMat []float64, acc *qrSweep, k, n int) {
|
||
a := tMat[k*n+k]
|
||
b := tMat[k*n+k+1]
|
||
d := tMat[(k+1)*n+(k+1)]
|
||
if b == 0 {
|
||
return
|
||
}
|
||
tau := (d - a) / (2 * b)
|
||
// Smaller root of t² + ((a−d)/b)·t − 1 = 0: t = −sign(τ)/(|τ|+√(1+τ²)).
|
||
t := -1.0 / (math.Abs(tau) + math.Sqrt(1+tau*tau))
|
||
if tau < 0 {
|
||
t = -t
|
||
}
|
||
c := 1 / math.Sqrt(1+t*t)
|
||
s := t * c
|
||
applyGivens(tMat, acc, k, n, c, s)
|
||
}
|
||
func wilkinsonShift2x2(d, e, f float64) float64 {
|
||
delta := (d - f) / 2
|
||
if delta == 0 {
|
||
return f - math.Abs(e)
|
||
}
|
||
sq := math.Sqrt(delta*delta + e*e)
|
||
if delta > 0 {
|
||
return f - e*e/(delta+sq)
|
||
}
|
||
return f + e*e/(sq-delta)
|
||
}
|
||
|
||
// tridiagQrStep performs one implicit-shift QR step on the symmetric
|
||
// tridiagonal block [l, h] of tMat, recording the rotations into acc
|
||
// for the batched eigenvector accumulation. Every rotation is a
|
||
// similarity transform applied to full rows and columns, so tMat stays
|
||
// exactly similar to its input; the pair (c, s) comes from the
|
||
// subdiagonal and the bulge the previous rotation left behind, the
|
||
// bulge chase of Golub & Van Loan §8.5.
|
||
//
|
||
// The tMat passes are strictly sequential and must stay so: rotation
|
||
// k+1 annihilates the bulge rotation k wrote, and the rounding-level
|
||
// entries far from the band drift inward one diagonal per pass, so
|
||
// narrowing or reordering any pass would move bits. Only the qMat mix
|
||
// is free of that chain; it is replayed from the recording, in order,
|
||
// by acc.flush.
|
||
func tridiagQrStep(tMat []float64, acc *qrSweep, l, h, n int, shift float64) {
|
||
// The first rotation is read from the leading entry of (T − μI);
|
||
// later ones annihilate the chased bulge.
|
||
x := tMat[l*n+l] - shift
|
||
y := tMat[l*n+(l+1)]
|
||
for k := l; k < h; k++ {
|
||
if k > l {
|
||
x = tMat[(k-1)*n+k] // subdiagonal entry
|
||
y = tMat[(k-1)*n+(k+1)] // bulge to annihilate
|
||
}
|
||
r := math.Hypot(x, y)
|
||
if r == 0 {
|
||
return // nothing to rotate; the band is already clean
|
||
}
|
||
c, s := x/r, y/r
|
||
// Rows k, k+1: left multiplication by Gᵀ, G = [[c, −s], [s, c]].
|
||
for j := range n {
|
||
tk := tMat[k*n+j]
|
||
tk1 := tMat[(k+1)*n+j]
|
||
tMat[k*n+j] = c*tk + s*tk1
|
||
tMat[(k+1)*n+j] = -s*tk + c*tk1
|
||
}
|
||
// Columns k, k+1: right multiplication by G.
|
||
for i := range n {
|
||
ti := tMat[i*n+k]
|
||
ti1 := tMat[i*n+k+1]
|
||
tMat[i*n+k] = c*ti + s*ti1
|
||
tMat[i*n+k+1] = -s*ti + c*ti1
|
||
}
|
||
// Eigenvector accumulation on the right: Q = Q·G, replayed in
|
||
// order by acc.flush.
|
||
acc.record(k, c, s)
|
||
}
|
||
}
|
||
|
||
// householder represents a Householder reflection v and its beta.
|
||
type householder struct {
|
||
v []float64
|
||
beta float64
|
||
}
|
||
|
||
// householderVectorInto builds the standard reflect-to-first-coordinate
|
||
// Householder vector for x, where H = I - beta v vᵀ with H x = sign(x₀)‖x‖ e₁,
|
||
// writing the reflector into dst (which must have room for len(x)
|
||
// entries and may alias x) so sweep callers can reuse one buffer
|
||
// instead of allocating per reflection. Squared magnitudes are summed
|
||
// relative to the largest entry so a vector with entries near 1e154
|
||
// does not overflow on the way to its norm.
|
||
//
|
||
// beta = 2/(vᵀv) needs the square of v's largest entry to stay in the
|
||
// normal range. Past about 1.3e154 that square is +Inf and beta comes
|
||
// out as +0, which callers read as "no reflection at all", and below
|
||
// about 1e-154 it is subnormal or 0 and beta comes out as +Inf, whose
|
||
// product with a zero dot is NaN. Either way the reflector is silently
|
||
// lost. When the largest entry falls outside that range the reflector is
|
||
// re-expressed in units of the exact power of two that brings it into
|
||
// the safe window: v is scaled and beta carries the compensatory square,
|
||
// so H is the same reflection with every intermediate O(1).
|
||
func householderVectorInto(dst, x []float64) householder {
|
||
maxAbs := 0.0
|
||
for _, xi := range x {
|
||
if v := math.Abs(xi); v > maxAbs {
|
||
maxAbs = v
|
||
}
|
||
}
|
||
if maxAbs == 0 {
|
||
return householder{v: x, beta: 0}
|
||
}
|
||
scaled2 := 0.0
|
||
for _, xi := range x {
|
||
xi /= maxAbs
|
||
scaled2 += xi * xi
|
||
}
|
||
norm := maxAbs * math.Sqrt(scaled2)
|
||
dst[0] = x[0] + signOrNonZero(x[0])*norm
|
||
copy(dst[1:], x[1:])
|
||
vMax := 0.0
|
||
for _, vi := range dst {
|
||
if v := math.Abs(vi); v > vMax {
|
||
vMax = v
|
||
}
|
||
}
|
||
if vMax == 0 {
|
||
return householder{v: dst, beta: 0}
|
||
}
|
||
v2 := 0.0
|
||
for _, vi := range dst {
|
||
vi /= vMax
|
||
v2 += vi * vi
|
||
}
|
||
if beta := 2 / (vMax * vMax * v2); beta > 0 && beta < math.MaxFloat64 {
|
||
return householder{v: dst, beta: beta}
|
||
}
|
||
ws := windowScale(vMax)
|
||
scaleFloats(dst, ws)
|
||
// w keeps max|v| inside the safe window, so w*w is a normal finite
|
||
// number and 2/(w*w*v2) is finite and non-zero: the same reflection,
|
||
// still built so that H x = sign(x₀)‖x‖ e₁.
|
||
w := vMax * ws
|
||
return householder{v: dst, beta: 2 / (w * w * v2)}
|
||
}
|
||
|
||
// signOrNonZero returns the sign of x, with 0 mapping to +1 (Householder
|
||
// convention).
|
||
func signOrNonZero(x float64) float64 {
|
||
if x >= 0 {
|
||
return 1
|
||
}
|
||
return -1
|
||
}
|
||
|
||
// isSymmetric reports whether the matrix is symmetric within a purely
|
||
// relative tolerance: a floor would admit an asymmetry that is a large
|
||
// fraction of a small-scale matrix, so the guard would call a matrix
|
||
// symmetric that is not, whatever the absolute size of its entries.
|
||
func isSymmetric(mat []float64, n int) bool {
|
||
scale := 0.0
|
||
for i := range n {
|
||
for j := range n {
|
||
a := math.Abs(mat[i*n+j])
|
||
if a > scale {
|
||
scale = a
|
||
}
|
||
}
|
||
}
|
||
tol := 1e-12 * scale
|
||
for i := range n {
|
||
for j := i + 1; j < n; j++ {
|
||
if math.Abs(mat[i*n+j]-mat[j*n+i]) > tol {
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
func countAboveThreshold(s []float64, eps float64, m, n int) int {
|
||
if eps <= 0 {
|
||
dim := max(n, m)
|
||
biggest := 0.0
|
||
for _, v := range s {
|
||
if v > biggest {
|
||
biggest = v
|
||
}
|
||
}
|
||
eps = float64(dim) * biggest * base.EpsF
|
||
}
|
||
count := 0
|
||
for _, v := range s {
|
||
if v > eps {
|
||
count++
|
||
}
|
||
}
|
||
return count
|
||
}
|
||
|
||
func condValue(s []float64, eps float64, m, n int) float64 {
|
||
if len(s) == 0 {
|
||
return 0
|
||
}
|
||
biggest := 0.0
|
||
smallest := math.Inf(1)
|
||
for _, v := range s {
|
||
if v > biggest {
|
||
biggest = v
|
||
}
|
||
if v < smallest {
|
||
smallest = v
|
||
}
|
||
}
|
||
if biggest == 0 {
|
||
// The zero matrix has no usable inverse direction; the condition
|
||
// number is infinite, matching the doc contract.
|
||
return math.Inf(1)
|
||
}
|
||
if smallest <= 0 {
|
||
return math.Inf(1)
|
||
}
|
||
if eps > 0 && smallest <= eps {
|
||
return math.Inf(1)
|
||
}
|
||
return biggest / smallest
|
||
}
|
||
|
||
func permuteCols(a []float64, rows, cols int, idx []int) {
|
||
out := make([]float64, rows*cols)
|
||
for j := range cols {
|
||
for i := range rows {
|
||
out[i*cols+j] = a[i*cols+idx[j]]
|
||
}
|
||
}
|
||
copy(a, out)
|
||
}
|
||
|
||
// sortDescIndices orders the indices so the values descend. The sort is
|
||
// stable, matching the insertion sort it replaced bit for bit: ties keep
|
||
// their original order, so the returned permutation is identical.
|
||
func sortDescIndices(s []float64) []int {
|
||
idx := make([]int, len(s))
|
||
for i := range idx {
|
||
idx[i] = i
|
||
}
|
||
slices.SortStableFunc(idx, func(a, b int) int {
|
||
switch {
|
||
case s[a] > s[b]:
|
||
return -1
|
||
case s[a] < s[b]:
|
||
return 1
|
||
default:
|
||
return 0
|
||
}
|
||
})
|
||
return idx
|
||
}
|
||
|
||
// sortAscIndices orders the indices so the values ascend, stable like
|
||
// sortDescIndices.
|
||
func sortAscIndices(s []float64) []int {
|
||
idx := make([]int, len(s))
|
||
for i := range idx {
|
||
idx[i] = i
|
||
}
|
||
slices.SortStableFunc(idx, func(a, b int) int {
|
||
switch {
|
||
case s[a] < s[b]:
|
||
return -1
|
||
case s[a] > s[b]:
|
||
return 1
|
||
default:
|
||
return 0
|
||
}
|
||
})
|
||
return idx
|
||
}
|
||
|
||
// isIdentityPerm reports whether the permutation is the identity.
|
||
func isIdentityPerm(p []int) bool {
|
||
for i, v := range p {
|
||
if v != i {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|