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
|
|
|
|
|
|
|
|
|
|
|
|
// 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)
|
2026-09-27 11:12:43 +02:00
|
|
|
|
// A non-finite entry slips through the symmetry test below: the
|
|
|
|
|
|
// difference of two NaNs never exceeds the tolerance, so a poisoned
|
|
|
|
|
|
// matrix reads as symmetric and the sweep hands back a NaN spectrum
|
|
|
|
|
|
// with a nil error. The sparse twin refuses the same input, and so
|
|
|
|
|
|
// does this one.
|
|
|
|
|
|
for i := range n {
|
|
|
|
|
|
for j := range n {
|
|
|
|
|
|
if math.IsNaN(mat[i*n+j]) || math.IsInf(mat[i*n+j], 0) {
|
|
|
|
|
|
return nil, nil, base.Errf("Eigen: entry [%d,%d] is not finite", i, j)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-03 10:00:00 +02:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|