Files
tensor/linalg/sparsecomplex.go
petrbalvin af4ee19703
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s
feat: initial release
Assisted-by: GLM 5.3 Flash
2026-09-03 10:00:00 +02:00

746 lines
22 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package linalg
import (
"math"
"sourcedock.dev/petrbalvin/tensor/internal/base"
"sourcedock.dev/petrbalvin/tensor/internal/core"
"sourcedock.dev/petrbalvin/tensor/internal/engine"
)
// Complex sparse linear algebra. The real sparse surface
// stores float64 payloads; the complex side mirrors it with a complex
// CSR whose values are complex128, built from a core.SparseCOO holding
// complex values (NewSparseCOO accepts them; SparseFrom does not, so
// callers assemble the COO directly). The solvers are complex
// conjugate gradient for Hermitian positive-definite systems, complex
// BiCGSTAB for general ones, and a Hermitian Lanczos eigensolver; the
// electromagnetics Helmholtz problems that motivated this surface hit
// all three.
// complexCSR is the complex128 compressed sparse row form.
type complexCSR struct {
rowStart []int
colIdx []int
vals []complex128
n int
}
// cEntry is one coordinate entry on the way into CSR form.
type cEntry struct {
row, col int
val complex128
}
// stableOrderComplexEntries orders entries by (row, col) and returns
// the slice holding the result. Two stable counting passes reach the
// order a stable comparison sort by (row, col) reaches, in linear time:
// the first places by column, the second by row, so entries that share
// both coordinates keep their input order and their values accumulate
// in it.
func stableOrderComplexEntries(entries []cEntry, rows, cols int) []cEntry {
buf := make([]cEntry, len(entries))
count := make([]int, max(rows, cols)+1)
countingPlace(buf, entries, count, func(e cEntry) int { return e.col })
countingPlace(entries, buf, count, func(e cEntry) int { return e.row })
return entries
}
// cooToComplexCSR converts a complex-valued SparseCOO to CSR form,
// merging duplicate coordinates and dropping explicit zeros. The
// values must be complex128; anything else is an error naming the
// dtype.
func cooToComplexCSR(s *core.SparseCOO, name string) (*complexCSR, error) {
if s.Values.Dtype() != core.Complex {
return nil, base.Errf("%s: needs complex128 values, got %s", name, s.Values.Dtype())
}
if len(s.Shape) != 2 {
return nil, base.Errf("%s: needs a 2-D matrix, got rank %d", name, len(s.Shape))
}
rows, cols := s.Shape[0], s.Shape[1]
if rows != cols {
return nil, base.Errf("%s: needs a square matrix, got %d×%d", name, rows, cols)
}
nnz := s.Indices.Shape()[0]
idx := s.Indices.RawInts()
entries := make([]cEntry, nnz)
for i := range nnz {
r := int(idx[i*2])
c := int(idx[i*2+1])
if r < 0 || r >= rows || c < 0 || c >= cols {
return nil, base.Errf("%s: index [%d,%d] out of range for %d×%d", name, r, c, rows, cols)
}
entries[i] = cEntry{r, c, s.Values.ComplexAt(i)}
}
// Sort by (row, col) so duplicates merge and rows are contiguous,
// keeping equal coordinates in COO order.
sorted := stableOrderComplexEntries(entries, rows, cols)
cm := &complexCSR{
rowStart: make([]int, rows+1),
colIdx: make([]int, 0, nnz),
vals: make([]complex128, 0, nnz),
n: rows,
}
// The duplicates are adjacent after the sort, so each run merges into
// one accumulated value on the way into the compressed form.
for p := 0; p < len(sorted); {
e := sorted[p]
v := e.val
p++
for p < len(sorted) && sorted[p].row == e.row && sorted[p].col == e.col {
v += sorted[p].val
p++
}
if v == 0 {
continue
}
cm.colIdx = append(cm.colIdx, e.col)
cm.vals = append(cm.vals, v)
cm.rowStart[e.row+1]++
}
for i := range cm.n {
cm.rowStart[i+1] += cm.rowStart[i]
}
return cm, nil
}
// checkHermitian verifies every stored entry has a conjugate mirror,
// the property the complex CG solver and the Hermitian Lanczos rely
// on. The tolerance is purely relative to the largest magnitude present,
// deliberately without an absolute floor: a floor would approve a matrix
// of small scale whose asymmetry is a large fraction of it, and the
// Jacobi and Lanczos routes would then answer for a matrix that is
// neither A nor Aᴴ. Relative-only keeps the approval and the arithmetic
// in agreement. A non-finite entry is refused in its own right: NaN
// compares unequal to everything, so the mirror test alone would wave
// it through as Hermitian. The wording of the refusal is unchanged.
func (c *complexCSR) checkHermitian(name string) error {
scale := 0.0
for _, v := range c.vals {
if a := cmplxAbs(v); a > scale {
scale = a
}
}
tol := 1e-12 * scale
for i := range c.n {
for p := c.rowStart[i]; p < c.rowStart[i+1]; p++ {
j := c.colIdx[p]
v := c.vals[p]
if math.IsNaN(real(v)) || math.IsNaN(imag(v)) ||
math.IsInf(real(v), 0) || math.IsInf(imag(v), 0) {
return base.Errf("%s: entry [%d,%d] is not finite", name, i, j)
}
mirror, ok := c.at(j, i)
if !ok || cmplxAbs(mirror-complexConj(v)) > tol {
return base.Errf("%s: matrix is not Hermitian within 1e-12 tolerance", name)
}
}
}
return nil
}
func complexConj(z complex128) complex128 { return complex(real(z), -imag(z)) }
func cmplxAbs(z complex128) float64 { return math.Hypot(real(z), imag(z)) }
// at reads the entry (row, col), reporting whether it is stored. The
// row's column indices are sorted, so the lookup is a binary search:
// the Hermitian check calls it once per stored entry, and a linear
// scan made that check quadratic in the row length.
func (c *complexCSR) at(row, col int) (complex128, bool) {
lo, hi := c.rowStart[row], c.rowStart[row+1]
for lo < hi {
mid := int(uint(lo+hi) >> 1)
if c.colIdx[mid] < col {
lo = mid + 1
} else {
hi = mid
}
}
if lo < c.rowStart[row+1] && c.colIdx[lo] == col {
return c.vals[lo], true
}
return 0, false
}
// complexMatVecRange computes y[i] = A[i,:]·x for the rows in [s, e) of
// a matrix in compressed row form. It is the whole body of the product,
// so the serial path calls it directly and the parallel path calls it
// per chunk: taking the structure slices as arguments rather than as
// captured variables keeps the serial path free of the closure a row
// split needs, which the complex solvers call once per iteration.
func complexMatVecRange(s, e int, rowStart, colIdx []int, values, x, y []complex128) {
for i := s; i < e; i++ {
sum := 0i
for p := rowStart[i]; p < rowStart[i+1]; p++ {
sum += values[p] * x[colIdx[p]]
}
y[i] = sum
}
}
// matVec computes y = A·x over complex vectors. Output rows are
// independent, so the row range splits across workers once a worker's
// share of the stored entries pays for the split, and runs on the
// calling goroutine below that.
func (c *complexCSR) matVec(x, y []complex128) {
// The three structure slices are taken once: every worker's rows walk
// them per stored entry, and the row loop holds no other state.
rowStart := c.rowStart
colIdx := c.colIdx
vals := c.vals
if sparseMatVecSplit(c.n, len(vals)) {
engine.ParallelMin(c.n, 1, func(s, e int) {
complexMatVecRange(s, e, rowStart, colIdx, vals, x, y)
})
return
}
complexMatVecRange(0, c.n, rowStart, colIdx, vals, x, y)
}
// complexVecF64 flattens a rank-1 complex array, rejecting anything
// else loudly.
func complexVecF64(b *core.Array, n int, name string) ([]complex128, error) {
if b.NDim() != 1 || b.Len() != n {
return nil, base.Errf("%s: b must be a rank-1 vector of length %d", name, n)
}
if b.Dtype() != core.Complex {
return nil, base.Errf("%s: b must be complex128, got %s", name, b.Dtype())
}
out := make([]complex128, n)
if !b.Strided() {
copy(out, b.RawComplexes()[:n])
return out, nil
}
for i := range n {
out[i] = b.ComplexAt(i)
}
return out, nil
}
// complexFromArray builds the result array from a complex vector.
func complexFromArray(v []complex128) (*core.Array, error) {
out, err := zeros(core.Complex, []int{len(v)})
if err != nil {
return nil, err
}
copy(out.RawComplexes(), v)
return out, nil
}
// SpSolveComplexCG returns x solving A·x = b for a Hermitian
// positive-definite sparse A with complex entries, by conjugate
// gradient with a complex Jacobi (diagonal) preconditioner. The
// stopping rule mirrors SpSolve: ‖b − A·x‖₂ ≤ tol·‖b‖₂, tol ≤ 0 uses
// 1e-10, maxIter ≤ 0 uses n, and an unconverged solve is an error
// naming the achieved residual rather than a silent approximation. A
// zero or missing diagonal entry is refused, exactly as the real
// solver refuses it.
func SpSolveComplexCG(a *core.SparseCOO, b *core.Array, tol float64, maxIter int) (*core.Array, error) {
const name = "SpSolveComplexCG"
c, err := cooToComplexCSR(a, name)
if err != nil {
return nil, err
}
if err := c.checkHermitian(name); err != nil {
return nil, err
}
n := c.n
bv, err := complexVecF64(b, n, name)
if err != nil {
return nil, err
}
if tol <= 0 {
tol = spSolveTol
}
if maxIter <= 0 {
maxIter = n
}
diag := make([]complex128, n)
for i := range n {
d, ok := c.at(i, i)
if !ok || d == 0 {
return nil, base.Errf("%s: zero diagonal entry at row %d", name, i)
}
diag[i] = d
}
// The system is solved in scaled units, which leaves the solution x
// untouched: A·x = b and (f·A)·x = (f·b) have the same x. The factor
// is 1 for any ordinary magnitude, so the recurrence stays
// bit-identical there, and outside that range it is what keeps the
// dot products and the squared magnitudes in the recurrence from
// overflowing or underflowing to nothing.
ws := scaleSystem(c, bv, diag)
bNorm := normC(bv)
if bNorm == 0 {
return complexFromArray(make([]complex128, n))
}
x := make([]complex128, n)
r := append([]complex128(nil), bv...)
p := make([]complex128, n)
z := make([]complex128, n)
Ap := make([]complex128, n)
for i := range n {
z[i] = r[i] / diag[i]
}
copy(p, z)
rz := dotC(r, z)
for iter := range maxIter {
c.matVec(p, Ap)
pAp := dotC(p, Ap)
if !cFinite(pAp) || pAp == 0 {
return nil, base.Errf("%s: breakdown at step %d (p·A·p vanished)", name, iter+1)
}
alpha := rz / pAp
for i := range n {
x[i] += alpha * p[i]
r[i] -= alpha * Ap[i]
}
if normC(r) <= tol*bNorm {
return complexFromArray(x)
}
for i := range n {
z[i] = r[i] / diag[i]
}
rzNext := dotC(r, z)
if rzNext == 0 {
return nil, base.Errf("%s: breakdown at step %d (r·z vanished)", name, iter+1)
}
beta := rzNext / rz
for i := range n {
p[i] = z[i] + beta*p[i]
}
rz = rzNext
}
// The residual and tolerance are reported in the caller's units, not
// in the scaled units the recurrence ran in (ws is 1 unless the
// system was extreme).
return nil, base.Errf("%s: no convergence in %d steps, residual %.3g (tolerance %.3g)",
name, maxIter, normC(r)/ws, tol*bNorm/ws)
}
// dotC is the complex inner product aᴴ·b (conjugating the left side,
// the convention every complex Krylov method uses).
func dotC(a, b []complex128) complex128 {
s := 0i
for i := range a {
s += complexConj(a[i]) * b[i]
}
return s
}
// normC is the Euclidean norm of a complex vector. The squares are
// summed directly while the sum stays finite and non-zero, which is what
// the stopping rules of the solvers were tuned on and is bit-for-bit the
// magnitude of every ordinary vector. A sum that overflows to +Inf or
// underflows to 0 (the whole vector below about 1e-162) carries no
// magnitude at all, so it falls back to a max-scaled accumulation that
// does: the magnitude has to exist for a relative convergence test to
// mean anything.
func normC(a []complex128) float64 {
s := 0.0
for _, v := range a {
s += real(v)*real(v) + imag(v)*imag(v)
}
if s == 0 || math.IsInf(s, 1) {
return normCScaled(a)
}
return math.Sqrt(s)
}
// normCScaled sums the squares relative to the largest magnitude, so no
// intermediate leaves the normal float64 range.
func normCScaled(a []complex128) float64 {
maxAbs := 0.0
for _, v := range a {
if m := cmplxAbs(v); m > maxAbs {
maxAbs = m
}
}
if maxAbs == 0 {
return 0
}
s := 0.0
for _, v := range a {
re := real(v) / maxAbs
im := imag(v) / maxAbs
s += re*re + im*im
}
return maxAbs * math.Sqrt(s)
}
// scaleSystem moves a system's matrix, right-hand side and preconditioner
// into the magnitude window the Krylov recurrences are safe in, by one
// shared exact power of two, and reports that factor. A·x = b and
// (f·A)·x = (f·b) have the same solution x, so the factor cancels: it
// only has to come off the residual and tolerance the error paths print
// back. Inside the window the dot products, the squared magnitudes and
// the stopping rule's tol*bNorm all mean what they claim; outside it a
// huge b makes bNorm +Inf (every affine check passes) and a tiny one
// makes it 0 (the zero solution is returned as exact).
func scaleSystem(c *complexCSR, bv, diag []complex128) float64 {
ws := windowScale(math.Max(maxMagComplex(c.vals), maxMagComplex(bv)))
if ws != 1 {
scaleComplexes(c.vals, ws)
scaleComplexes(bv, ws)
scaleComplexes(diag, ws)
}
return ws
}
// SpSolveComplexBiCGSTAB returns x solving A·x = b for a general
// (nonsymmetric, non-Hermitian) sparse A with complex entries, by
// BiCGSTAB with a complex Jacobi preconditioner. The contract mirrors
// SpSolveBiCGSTAB, including the affirmative convergence checks a NaN
// residual can never pass and the breakdown errors that name the step.
func SpSolveComplexBiCGSTAB(a *core.SparseCOO, b *core.Array, tol float64, maxIter int) (*core.Array, error) {
const name = "SpSolveComplexBiCGSTAB"
c, err := cooToComplexCSR(a, name)
if err != nil {
return nil, err
}
n := c.n
bv, err := complexVecF64(b, n, name)
if err != nil {
return nil, err
}
if tol <= 0 {
tol = spSolveTol
}
if maxIter <= 0 {
maxIter = n
}
diag := make([]complex128, n)
for i := range n {
d, ok := c.at(i, i)
if !ok || d == 0 {
return nil, base.Errf("%s: zero diagonal entry at row %d", name, i)
}
diag[i] = d
}
// The system is solved in scaled units, which leaves the solution x
// untouched, exactly as in SpSolveComplexCG above.
ws := scaleSystem(c, bv, diag)
bNorm := normC(bv)
if bNorm == 0 {
return complexFromArray(make([]complex128, n))
}
r := append([]complex128(nil), bv...)
rHat := append([]complex128(nil), r...)
x := make([]complex128, n)
v := make([]complex128, n)
p := make([]complex128, n)
s := make([]complex128, n)
t := make([]complex128, n)
precond := func(dst, src []complex128) {
for i := range n {
dst[i] = src[i] / diag[i]
}
}
rho := 1 + 0i
alpha := 1 + 0i
omega := 1 + 0i
z := make([]complex128, n)
y := make([]complex128, n)
mt := make([]complex128, n)
for iter := range maxIter {
rhoNext := dotC(rHat, r)
if !cFinite(rhoNext) || rhoNext == 0 {
return nil, base.Errf("%s: breakdown at step %d (rho vanished)", name, iter+1)
}
if iter > 0 {
beta := rhoNext / rho * (alpha / omega)
for i := range n {
p[i] = r[i] + beta*(p[i]-omega*v[i])
}
} else {
copy(p, r)
}
precond(z, p)
c.matVec(z, v)
rhatV := dotC(rHat, v)
if rhatV == 0 {
return nil, base.Errf("%s: breakdown at step %d (direction orthogonal to shadow)", name, iter+1)
}
alpha = rhoNext / rhatV
for i := range n {
s[i] = r[i] - alpha*v[i]
}
if !vecFiniteC(s) {
return nil, base.Errf("%s: breakdown at step %d (non-finite residual)", name, iter+1)
}
// The half step may already be exact. The check is affirmative
// so a NaN norm (which fails every comparison) can never read
// as converged.
if normC(s) > tol*bNorm {
precond(y, s)
c.matVec(y, t)
precond(mt, t)
mtS := dotC(mt, s)
mtT := dotC(mt, t)
if mtT == 0 {
return nil, base.Errf("%s: breakdown at step %d (stabiliser vanished)", name, iter+1)
}
omega = mtS / mtT
if !cFinite(omega) {
return nil, base.Errf("%s: breakdown at step %d (stabiliser vanished)", name, iter+1)
}
for i := range n {
x[i] += alpha*z[i] + omega*y[i]
r[i] = s[i] - omega*t[i]
}
if !vecFiniteC(r) {
return nil, base.Errf("%s: breakdown at step %d (non-finite residual)", name, iter+1)
}
if normC(r) <= tol*bNorm {
return complexFromArray(x)
}
if omega == 0 {
return nil, base.Errf("%s: stagnation at step %d (omega zero)", name, iter+1)
}
} else {
for i := range n {
x[i] += alpha * z[i]
}
return complexFromArray(x)
}
rho = rhoNext
}
// The residual and tolerance are reported in the caller's units, not
// in the scaled units the recurrence ran in (ws is 1 unless the
// system was extreme).
return nil, base.Errf("%s: no convergence in %d steps, residual %.3g (tolerance %.3g)",
name, maxIter, normC(r)/ws, tol*bNorm/ws)
}
func cFinite(z complex128) bool {
return !math.IsNaN(real(z)) && !math.IsInf(real(z), 0) &&
!math.IsNaN(imag(z)) && !math.IsInf(imag(z), 0)
}
func vecFiniteC(v []complex128) bool {
for _, z := range v {
if !cFinite(z) {
return false
}
}
return true
}
// SpEigenComplex returns the k eigenvalues of largest magnitude of a
// Hermitian sparse matrix with complex entries, each with its unit
// eigenvector, by the Lanczos recurrence over complex vectors. The
// contract mirrors SpEigen: values ordered by descending magnitude,
// eigenvectors as columns of an (n, k) complex array, approximate
// Ritz pairs whose accuracy improves with the iteration budget. A
// non-Hermitian matrix is refused; the general complex eigenproblem
// has no short recurrence and belongs to dense methods.
func SpEigenComplex(s *core.SparseCOO, k int, gen *core.Generator) (values, vectors *core.Array, err error) {
const name = "SpEigenComplex"
if len(s.Shape) != 2 || s.Shape[0] != s.Shape[1] {
return nil, nil, base.Errf("%s: needs a square 2-D sparse matrix, got shape %v", name, s.Shape)
}
n := s.Shape[0]
if n == 0 {
return nil, nil, base.Errf("%s: zero-sized matrix, got shape %v", name, s.Shape)
}
if k < 1 || k > n {
return nil, nil, base.Errf("%s: k must be in [1, %d], got %d", name, n, k)
}
c, err := cooToComplexCSR(s, name)
if err != nil {
return nil, nil, err
}
if err := c.checkHermitian(name); err != nil {
return nil, nil, err
}
// The recurrence is projected in scaled units: the alphas and betas
// carry the matrix's magnitude, the basis vectors do not, and a
// power-of-two factor multiplies the first two exactly while leaving
// the third bit-identical, so only the Ritz values have to be moved
// back. Without it the projected tridiagonal's squared magnitudes
// overflow at the top of the range and the recurrence collapses to
// NaN, and at the bottom the deflation floor sees zeros.
ws := windowScale(maxMagComplex(c.vals))
if ws != 1 {
scaleComplexes(c.vals, ws)
}
if gen == nil {
gen = core.NewGenerator(spEigenSeed)
}
alphas, betas, basis := c.lanczosComplex(k, gen)
r := len(alphas)
tMat := make([]float64, r*r)
for i := range r {
tMat[i*r+i] = alphas[i]
if i+1 < r {
tMat[i*r+i+1] = betas[i]
tMat[(i+1)*r+i] = betas[i]
}
}
tVec := eye(r)
if err := symmetricQr(tMat, tVec, r); err != nil {
return nil, nil, base.Errf("SpEigenComplex: %w", err)
}
idx := make([]int, r)
for i := range r {
idx[i] = i
}
// Rank by descending magnitude, the same comparator SpEigen uses.
for i := 1; i < r; i++ {
for j := i; j > 0; j-- {
a, b := idx[j-1], idx[j]
da, db := math.Abs(tMat[a*r+a]), math.Abs(tMat[b*r+b])
if da > db || (da == db && a <= b) {
break
}
idx[j-1], idx[j] = idx[j], idx[j-1]
}
}
sel := idx[:k]
outVals := make([]float64, k)
outVecs := make([]complex128, n*k)
tmp := make([]complex128, n)
for j, si := range sel {
outVals[j] = tMat[si*r+si]
// Ritz vector: lift the real tridiagonal eigenvector through
// the complex Lanczos basis, v = Q·y.
clear(tmp)
for p := range r {
y := tVec[p*r+si]
if y == 0 {
continue
}
row := basis[p*n : (p+1)*n]
for i := range n {
tmp[i] += complex(y, 0) * row[i]
}
}
norm := normC(tmp)
if norm > 0 {
for i := range n {
tmp[i] /= complex(norm, 0)
}
}
for i := range n {
outVecs[i*k+j] = tmp[i]
}
}
if ws != 1 {
unscaleFloats(outVals, ws)
}
vecs, err := complexFromArray2D(outVecs, n, k)
if err != nil {
return nil, nil, err
}
return floatsToArray(outVals, []int{k}), vecs, nil
}
// complexFromArray2D builds an (n, k) complex array from row-major
// values.
func complexFromArray2D(v []complex128, n, k int) (*core.Array, error) {
out, err := zeros(core.Complex, []int{n, k})
if err != nil {
return nil, err
}
copy(out.RawComplexes(), v)
return out, nil
}
// lanczosComplex runs the Hermitian Lanczos recurrence over complex
// vectors: alphas stay real (qᴴAq of a Hermitian A), betas stay real
// (vector norms), so the projected problem reuses the real symmetric
// tridiagonal eigensolver unchanged. Full reorthogonalisation and the
// purely scale-relative deflation floor mirror the real lanczos.
func (c *complexCSR) lanczosComplex(k int, gen *core.Generator) (alphas, betas []float64, basis []complex128) {
n := c.n
steps := min(n, max(2*k, k+spEigenBlock))
w := make([]complex128, n)
q := make([]complex128, n)
prev := make([]complex128, n)
// The budget bounds the recurrence: one basis row and at most one
// coefficient per step, so all three are sized once rather than grown.
alphas = make([]float64, 0, steps)
betas = make([]float64, 0, steps)
basis = make([]complex128, 0, steps*n)
scale := 0.0
addScale := func(v float64) {
if v > scale {
scale = v
}
}
start := func() {
for i := range n {
q[i] = complex(gen.NormalUnit(), gen.NormalUnit())
}
for p := range len(alphas) {
row := basis[p*n : (p+1)*n]
d := dotC(row, q)
for i := range n {
q[i] -= d * row[i]
}
}
if norm := normC(q); norm > 0 {
for i := range n {
q[i] /= complex(norm, 0)
}
}
}
start()
for range steps {
basis = append(basis, q...)
c.matVec(q, w)
alpha := real(dotC(q, w))
alphas = append(alphas, alpha)
addScale(math.Abs(alpha))
a := complex(alpha, 0)
for i := range n {
w[i] -= a * q[i]
}
if len(alphas) > 1 {
b := complex(betas[len(betas)-1], 0)
for i := range n {
w[i] -= b * prev[i]
}
}
// Full reorthogonalisation, twice: one pass removes the
// accumulated loss, the second what the first reintroduces.
for range 2 {
for p := range len(alphas) {
row := basis[p*n : (p+1)*n]
d := dotC(row, w)
for i := range n {
w[i] -= d * row[i]
}
}
}
beta := normC(w)
// A purely scale-relative exhaustion threshold; an absolute
// floor would collapse the recurrence for tiny-norm matrices.
if beta <= float64(n)*base.EpsF*scale {
if len(alphas) >= n {
break
}
betas = append(betas, 0)
start()
continue
}
betas = append(betas, beta)
addScale(beta)
copy(prev, q)
for i := range n {
q[i] = w[i] / complex(beta, 0)
}
}
if len(betas) >= len(alphas) {
betas = betas[:len(alphas)-1]
}
return alphas, betas, basis
}