Files
tensor/linalg/sparselsqr.go
T
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

808 lines
28 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"
)
// Overdetermined sparse least squares: min ‖A·x − b‖₂ for a sparse A
// with at least as many rows as columns. The dense `LeastSquares`
// factorises the whole matrix, which a large sparse system cannot
// afford and need not: LSQR and LSMR walk the Golub-Kahan
// bidiagonalisation instead, whose every step costs one A·v product
// and one Aᵀ·u product, so the cost tracks the non-zero count. LSQR
// minimises ‖b − A·x‖ over the growing Krylov space; LSMR minimises
// ‖Aᵀ(b − A·x)‖, which keeps the normal-equations residual monotone.
//
// Both solvers carry the stopping-criterion trio the roadmap names for
// this recursion (Paige and Saunders):
//
// - "residual": the estimate of ‖b − A·x‖₂ has fallen against the
// scaled target tol·(‖b‖ + ‖A‖·‖x‖);
// - "normal": the estimate of ‖Aᵀ(b − A·x)‖₂, the residual of the
// normal equations, has fallen against tol·‖A‖·‖b − A·x‖;
// - "condition": the iteration's conditional estimate of cond(A)
// has passed the limit conlim, the augmented estimate the
// recursion maintains from the bidiagonal entries.
//
// The iteration stops when ANY criterion fires, the standard LSQR and
// LSMR semantics, and the returned info records which one fired. When
// several fire on the same step the residual criterion wins, the
// priority the reference implementations apply. A condition stop is a
// stop, not a convergence, and Converged reports the difference.
//
// An underdetermined system (fewer rows than columns) is refused: the
// problem changes shape there, the minimum-norm answer of an
// underdetermined solve is a different contract than the least-squares
// answer this file implements, and the dense surface refuses it the
// same way.
//
// Both entry points are deterministic: every product runs serially
// over the stored entries in index order and no goroutine joins the
// iteration. The per-step allocation is nothing beyond the info's own
// estimate trajectory: the working vectors are allocated once and
// reused, and no step copies a matrix.
//
// Two guards keep the recursion honest where the plain recurrences
// would lie. A bidiagonal entry at or below 64·eps of the recursion's
// own scale, the constant `SolveTruncated` applies to a vanished
// singular value, carries no usable direction: it is treated as
// exactly zero, the current step folds on the clamped values and the
// iteration stops, because a direction the recursion cannot resolve
// would otherwise be amplified into the answer. And a fired criterion
// is verified against the explicitly recomputed residual before the
// answer is returned, so an estimate the drift of the recursion has
// detached from the truth can end the iteration only as an honest
// failure, never as a converged answer.
// spLeastSquaresTol is the default relative tolerance when the caller
// passes zero, matching the package's iterative solvers.
const spLeastSquaresTol = 1e-10
// spLeastSquaresConLim is the default condition limit when the caller
// passes zero: large enough that a healthy system never trips it, low
// enough that a hopeless one stops instead of burning its budget.
const spLeastSquaresConLim = 1e8
// spLeastSquaresBreakdown is the breakdown constant, in multiples of
// the machine epsilon relative to the recursion's own scale, below
// which a bidiagonal entry is treated as exactly zero.
const spLeastSquaresBreakdown = 64
// The stopping criteria a sparse least-squares iteration can report.
const (
// LeastSquaresResidual names the residual-norm criterion: the
// estimate of ‖b − A·x‖₂ met the scaled target.
LeastSquaresResidual = "residual"
// LeastSquaresNormal names the normal-equations criterion: the
// estimate of ‖Aᵀ(b − A·x)‖₂ met the scaled target.
LeastSquaresNormal = "normal"
// LeastSquaresCondition names the conditional criterion: the
// estimate of cond(A) passed the limit.
LeastSquaresCondition = "condition"
)
// LeastSquaresInfo reports what a sparse least-squares iteration
// achieved and which stopping test ended it.
type LeastSquaresInfo struct {
// Iterations is the number of Golub-Kahan steps folded into the
// answer.
Iterations int
// Criterion names the test that ended the iteration:
// LeastSquaresResidual, LeastSquaresNormal or
// LeastSquaresCondition. It is empty when the exact answer x = 0
// was returned without a step.
Criterion string
// ResidualNorm is the achieved ‖b − A·x‖₂, recomputed explicitly
// from the returned x once the iteration has ended, so it is the
// truth and not the in-loop estimate.
ResidualNorm float64
// NormalResidual is the achieved ‖Aᵀ(b − A·x)‖₂, recomputed the
// same way.
NormalResidual float64
// MatrixNorm is the iteration's running estimate of ‖A‖.
MatrixNorm float64
// Condition is the iteration's running estimate of cond(A).
Condition float64
// Converged reports whether a residual or normal-equations test
// fired. A condition stop leaves it false: the answer is the
// estimate reached when the condition limit passed, the standard
// LSQR and LSMR semantics, and an honest caller treats it as a
// warning, not a solution.
Converged bool
// residualEstimates and normalEstimates hold the per-step in-loop
// estimates of ‖b − A·x‖ and ‖Aᵀ(b − A·x)‖ in iteration order, so
// the convergence behaviour stays inspectable.
residualEstimates []float64
normalEstimates []float64
}
// lsqrOperator holds A and its transpose in CSR form, built once per
// solve so every iteration streams two products through contiguous
// slices instead of touching the coordinate form.
type lsqrOperator struct {
m, n int
rowStart []int
colIdx []int
vals []float64
tStart []int
tIdx []int
tVals []float64
}
// newLSQROperator converts the coordinate form through the canonical
// CSR conversion (duplicates sum, explicit zeros drop) and counts the
// transpose in one pass.
func newLSQROperator(a *core.SparseCOO) (*lsqrOperator, error) {
csr, err := CSRFromCOO(a)
if err != nil {
return nil, err
}
t := csr.Transpose()
return &lsqrOperator{
m: csr.Rows, n: csr.Cols,
rowStart: csr.RowStart, colIdx: csr.ColIdx, vals: csr.Values,
tStart: t.RowStart, tIdx: t.ColIdx, tVals: t.Values,
}, nil
}
// matVec writes A·x into out, one row at a time in ascending order.
func (op *lsqrOperator) matVec(x, out []float64) {
for i := range op.m {
sum := 0.0
for p := op.rowStart[i]; p < op.rowStart[i+1]; p++ {
sum += op.vals[p] * x[op.colIdx[p]]
}
out[i] = sum
}
}
// tMatVec writes Aᵀ·x into out, one column of A at a time in ascending
// order, which keeps the reduction order deterministic.
func (op *lsqrOperator) tMatVec(x, out []float64) {
for j := range op.n {
sum := 0.0
for p := op.tStart[j]; p < op.tStart[j+1]; p++ {
sum += op.tVals[p] * x[op.tIdx[p]]
}
out[j] = sum
}
}
// checkSparseLeastSquares validates the inputs both least-squares
// solvers share: a non-empty real 2-D sparse matrix with at least as
// many rows as columns and a real right-hand side of the row count. It
// returns the operator and b as a working vector.
func checkSparseLeastSquares(name string, a *core.SparseCOO, b *core.Array) (*lsqrOperator, []float64, error) {
if a.Values.Dtype() == core.Complex {
return nil, nil, base.Errf("%s: complex sparse matrices are not supported", name)
}
if len(a.Shape) != 2 || a.Shape[0] == 0 || a.Shape[1] == 0 {
return nil, nil, base.Errf("%s: needs a non-empty 2-D sparse matrix, got shape %v", name, a.Shape)
}
m, n := a.Shape[0], a.Shape[1]
if m < n {
return nil, nil, base.Errf("%s: needs an overdetermined system with m ≥ n, got %d×%d; the underdetermined minimum-norm problem is a different contract", name, m, n)
}
op, err := newLSQROperator(a)
if err != nil {
return nil, nil, base.Errf("%s: %w", name, err)
}
if b.Dtype() == core.Complex {
return nil, nil, base.Errf("%s: complex right-hand sides are not supported", name)
}
if b.NDim() != 1 || b.Len() != m {
return nil, nil, base.Errf("%s: right-hand side must be a vector of length %d, got shape %s",
name, m, base.ShapeText(b.Shape()))
}
return op, vectorF64(b, m), nil
}
// symOrtho computes the Givens rotation of the pair (a, b) that zeroes
// the second coordinate, in the stable form the reference
// implementations use: the signs are carried instead of cancelled, so
// no intermediate reaches 1/eps. r is the resulting non-negative norm.
func symOrtho(a, b float64) (c, s, r float64) {
switch {
case b == 0:
return sign(a), 0, math.Abs(a)
case a == 0:
return 0, sign(b), math.Abs(b)
case math.Abs(b) > math.Abs(a):
tau := a / b
s = sign(b) / math.Sqrt(1+tau*tau)
c = s * tau
r = b / s
default:
tau := b / a
c = sign(a) / math.Sqrt(1+tau*tau)
s = c * tau
r = a / c
}
return c, s, r
}
// lsqrExplicitNorms recomputes ‖b − A·x‖ and ‖Aᵀ(b − A·x)‖ directly
// from a candidate x: two products, run once at the end of the
// iteration, so the reported achieved quantities are the truth rather
// than the in-loop estimates. ax and ar are scratch vectors of the
// operator's row and column counts, taken over from the iteration that
// just ended.
func lsqrExplicitNorms(op *lsqrOperator, bv, x, ax, ar []float64) (rNorm, arNorm float64) {
op.matVec(x, ax)
for i := range op.m {
ax[i] = bv[i] - ax[i]
}
op.tMatVec(ax, ar)
return norm2F64(ax), norm2F64(ar)
}
// verifyLeastSquares checks a fired criterion against the explicitly
// recomputed norms, with the round-off slack the achieved norms can
// never beat, and relabels to the sibling test when the fired one
// fails while the other passes. It reports whether the achieved
// answer satisfies any residual test at all; a condition stop makes no
// accuracy claim and passes unverified.
func verifyLeastSquares(criterion string, rNorm, arNorm, bNorm, matrixNorm, xNorm, tol float64) (string, bool) {
targetR := tol*(bNorm+matrixNorm*xNorm) + 64*base.EpsF*(bNorm+matrixNorm*xNorm)
targetA := tol*matrixNorm*rNorm + 64*base.EpsF*matrixNorm*(bNorm+rNorm+xNorm)
passR := rNorm <= targetR
passA := arNorm <= targetA
switch criterion {
case LeastSquaresResidual:
if passR {
return LeastSquaresResidual, true
}
if passA {
return LeastSquaresNormal, true
}
case LeastSquaresNormal:
if passA {
return LeastSquaresNormal, true
}
if passR {
return LeastSquaresResidual, true
}
default:
return criterion, true
}
return criterion, false
}
// zeroLeastSquaresAnswer builds the answer both solvers give when the
// iteration needs no step: b is zero, or b is orthogonal to the column
// space of A, and x = 0 is then the exact least-squares solution.
func zeroLeastSquaresAnswer(n int, bNorm, alpha float64) (*core.Array, *LeastSquaresInfo, error) {
x, err := core.Zeros(core.Float, n)
if err != nil {
return nil, nil, err
}
info := &LeastSquaresInfo{
Criterion: "",
Converged: true,
ResidualNorm: bNorm,
NormalResidual: 0,
MatrixNorm: alpha,
Condition: 0,
}
return x, info, nil
}
// SpLSQR returns the vector x minimising ‖A·x − b‖₂ over a sparse
// overdetermined A, by the Golub-Kahan bidiagonalisation recursion of
// Paige and Saunders. The stopping criterion trio is the residual-norm
// estimate, the normal-equations residual and the conditional
// estimate; the iteration stops when ANY criterion fires and the
// returned info names it and carries the achieved quantities.
//
// tol ≤ 0 means 1e-10 and feeds both the residual and the
// normal-equations test; maxIter ≤ 0 means 2n steps, the reference
// default of twice the Krylov dimension; conlim ≤ 0 means 1e8, and
// cond(A) passing it stops the iteration without claiming
// convergence. Running out of steps with every tolerance unmet is an
// error naming the residual achieved, with no estimate returned, in
// the style of the package's other solvers.
func SpLSQR(a *core.SparseCOO, b *core.Array, tol float64, maxIter int, conlim float64) (*core.Array, *LeastSquaresInfo, error) {
const name = "SpLSQR"
op, bv, err := checkSparseLeastSquares(name, a, b)
if err != nil {
return nil, nil, err
}
if tol <= 0 {
tol = spLeastSquaresTol
}
if conlim <= 0 {
conlim = spLeastSquaresConLim
}
ctol := 1 / conlim
m, n := op.m, op.n
if maxIter <= 0 {
maxIter = 2 * n
}
x := make([]float64, n)
bNorm := norm2F64(bv)
if bNorm == 0 {
return zeroLeastSquaresAnswer(n, 0, 0)
}
// β₁u₁ = b, α₁v₁ = Aᵀu₁. A vanishing α₁ means b is orthogonal to
// the column space and x = 0 is the exact answer.
u := append([]float64(nil), bv...)
scale := 1 / bNorm
for i := range m {
u[i] *= scale
}
v := make([]float64, n)
op.tMatVec(u, v)
alpha := norm2F64(v)
if alpha == 0 {
return zeroLeastSquaresAnswer(n, bNorm, 0)
}
scale = 1 / alpha
for j := range n {
v[j] *= scale
}
w := append([]float64(nil), v...)
// The rotated right-hand side: φ̄ carries the part of b no step has
// consumed yet, ρ̄ the pending bidiagonal entry. The estimates are
// ‖b − A·x‖ = |φ̄| and the normal-equations estimate α·|τ|, τ being
// the rotated off-diagonal, exactly as the reference recursion
// maintains them.
rhobar, phibar := alpha, bNorm
anorm, ddnorm, xxnorm := 0.0, 0.0, 0.0
xnorm, z, cs2, sn2 := 0.0, 0.0, -1.0, 0.0
info := &LeastSquaresInfo{}
criterion := ""
exhausted := false
steps := 0
uBuf := make([]float64, m)
vBuf := make([]float64, n)
// The per-step estimates grow into buffers sized for the iteration
// budget, capped at the default budget so a caller's huge maxIter
// cannot allocate steps the recursion will not take.
estCap := min(maxIter, 2*n)
rEsts := make([]float64, 0, estCap)
arEsts := make([]float64, 0, estCap)
// hScale tracks the recursion's own magnitude for the breakdown
// floor: a bidiagonal entry at round-off of the scale already seen
// carries no direction.
hScale := max(alpha, bNorm)
// hitBudget tells the three exits apart: a loop that ended by its
// own iteration count fired no criterion and broke down nowhere,
// which is the budget's fault and not an estimate's drift.
hitBudget := true
for itn := 1; itn <= maxIter; itn++ {
// βu = A·v − αu, αv = Aᵀu − βv: one step of the bidiagonalisation.
op.matVec(v, uBuf)
for i := range m {
uBuf[i] -= alpha * u[i]
}
if !vecFinite(uBuf) {
return nil, nil, base.Errf("%s: non-finite residual direction at step %d", name, itn)
}
beta := norm2F64(uBuf)
hScale = max(hScale, beta)
floor := spLeastSquaresBreakdown * base.EpsF * hScale
if beta <= floor {
// The u-direction is gone: fold the pending step as the
// exact-arithmetic completion, with β = 0, and stop.
beta = 0
exhausted = true
} else {
scale = 1 / beta
for i := range m {
u[i] = uBuf[i] * scale
}
anorm = math.Sqrt(anorm*anorm + alpha*alpha + beta*beta)
op.tMatVec(u, vBuf)
for j := range n {
vBuf[j] -= beta * v[j]
}
if !vecFinite(vBuf) {
return nil, nil, base.Errf("%s: non-finite normal direction at step %d", name, itn)
}
alpha = norm2F64(vBuf)
hScale = max(hScale, alpha)
if alpha <= floor {
// The v-direction is gone: α is the exact zero the
// recursion would have produced, v the zero vector, and
// the iteration stops after this fold.
alpha = 0
exhausted = true
clear(v)
} else if alpha > 0 {
scale = 1 / alpha
for j := range n {
v[j] = vBuf[j] * scale
}
}
}
// The plane rotation that eliminates the subdiagonal β. With a
// clamped β the rotation reads zero off the subdiagonal, which
// is the exact-arithmetic completion of the recursion; a stale
// α only feeds ᾱ, which the stop leaves unused.
cs, sn, rho := symOrtho(rhobar, beta)
if rho == 0 {
exhausted = true
hitBudget = false
break
}
theta := sn * alpha
rhobar = -cs * alpha
phi := cs * phibar
phibar = sn * phibar
tau := sn * phi
t1 := phi / rho
t2 := -theta / rho
// ddnorm feeds the condition estimate: the accumulated squared
// lengths of the consumed directions, read before w moves.
nw := norm2F64(w)
ddnorm += nw * nw / (rho * rho)
for j := range n {
x[j] += t1 * w[j]
w[j] = v[j] + t2*w[j]
}
steps = itn
// ‖x‖ through a running rotation on the triangular solve, the
// reference recursion's incremental form. cs2 starts at −1 and
// every update carries a nonzero gambar, so the divisions never
// see zero.
delta := sn2 * rho
gambar := -cs2 * rho
rhs := phi - delta*z
zbar := rhs / gambar
xnorm = math.Sqrt(xxnorm + zbar*zbar)
gamma := math.Hypot(gambar, theta)
cs2 = gambar / gamma
sn2 = theta / gamma
z = rhs / gamma
xxnorm += z * z
rEst := math.Abs(phibar)
arEst := alpha * math.Abs(tau)
rEsts = append(rEsts, rEst)
arEsts = append(arEsts, arEst)
acond := anorm * math.Sqrt(ddnorm)
test1 := rEst / bNorm
test2 := arEst / (anorm*rEst + base.EpsF)
test3 := 1 / (acond + base.EpsF)
target := tol * (1 + anorm*xnorm/bNorm)
switch {
case test1 <= target:
criterion = LeastSquaresResidual
case test2 <= tol:
criterion = LeastSquaresNormal
case test3 <= ctol:
criterion = LeastSquaresCondition
}
if criterion != "" || exhausted {
hitBudget = false
break
}
}
rNorm, arNorm := lsqrExplicitNorms(op, bv, x, uBuf, vBuf)
criterion = settleLeastSquares(criterion, exhausted, rNorm, arNorm, x, bNorm, anorm, tol)
if criterion == "" {
if exhausted {
return nil, nil, base.Errf("%s: no convergence: the recursion folded at step %d, residual %.3g (tolerance %.3g)",
name, steps, rNorm, tol)
}
if hitBudget {
return nil, nil, base.Errf("%s: no convergence in %d steps, residual %.3g (tolerance %.3g)",
name, maxIter, rNorm, tol)
}
return nil, nil, base.Errf("%s: a stopping criterion fired at step %d but its estimate drifted past the recomputed norms (residual %.3g, tolerance %.3g)",
name, steps, rNorm, tol)
}
info.Iterations = steps
info.Criterion = criterion
info.Converged = criterion != LeastSquaresCondition
info.ResidualNorm = rNorm
info.NormalResidual = arNorm
info.MatrixNorm = anorm
info.Condition = anorm * math.Sqrt(ddnorm)
info.residualEstimates = rEsts
info.normalEstimates = arEsts
return floatsToArray(x, []int{n}), info, nil
}
// SpLSMR returns the vector x minimising ‖Aᵀ(b − A·x)‖₂ over a sparse
// overdetermined A, by Fong and Saunders' LSMR, the Golub-Kahan
// bidiagonalisation with the recurrences reorganised so that both
// ‖b − A·x‖ and ‖Aᵀ(b − A·x)‖ carry exact-norm estimates. The stopping
// criterion trio and the reporting contract are SpLSQR's; the
// normal-equations residual, LSMR's own minimisation target, moves
// monotonically. LSMR's answer on a consistent rank-deficient system
// is the minimum-norm one, as LSQR's is.
//
// maxIter ≤ 0 means 2n steps, twice the Krylov dimension, the same
// default SpLSQR applies; the breakdown floor ends the recursion
// before a step can fold a direction the space cannot resolve.
func SpLSMR(a *core.SparseCOO, b *core.Array, tol float64, maxIter int, conlim float64) (*core.Array, *LeastSquaresInfo, error) {
const name = "SpLSMR"
op, bv, err := checkSparseLeastSquares(name, a, b)
if err != nil {
return nil, nil, err
}
if tol <= 0 {
tol = spLeastSquaresTol
}
if conlim <= 0 {
conlim = spLeastSquaresConLim
}
ctol := 1 / conlim
m, n := op.m, op.n
if maxIter <= 0 {
maxIter = 2 * n
}
x := make([]float64, n)
bNorm := norm2F64(bv)
if bNorm == 0 {
return zeroLeastSquaresAnswer(n, 0, 0)
}
u := append([]float64(nil), bv...)
scale := 1 / bNorm
for i := range m {
u[i] *= scale
}
v := make([]float64, n)
op.tMatVec(u, v)
alpha := norm2F64(v)
if alpha == 0 {
return zeroLeastSquaresAnswer(n, bNorm, 0)
}
scale = 1 / alpha
for j := range n {
v[j] *= scale
}
// The two rotation pairs of LSMR: one turns the bidiagonal matrix
// upper triangular (ρ, c, s and the ᾱ carry), the other turns its
// transpose back (c̄, s̄, ρ̄), which is what makes ζ̄ the exact
// normal-equations norm the solver minimises.
zetabar := alpha * bNorm
alphabar := alpha
rho, rhobar := 1.0, 1.0
cbar, sbar := 1.0, 0.0
h := append([]float64(nil), v...)
hbar := make([]float64, n)
// The residual-norm bookkeeping: ζ, βd and β̆ walk the rotated
// right-hand side so ‖r‖ = √((βd − τd)² + β̆²) is exact in exact
// arithmetic.
betadd, betad := bNorm, 0.0
rhodold, tautildeold, thetatilde, zeta := 1.0, 0.0, 0.0, 0.0
// The ‖A‖ and cond(A) estimates: the squared bidiagonal entries and
// the extreme rotated diagonals.
normA2 := alpha * alpha
maxrbar, minrbar := 0.0, 1e100
info := &LeastSquaresInfo{}
criterion := ""
exhausted := false
steps := 0
uBuf := make([]float64, m)
vBuf := make([]float64, n)
// The estimate buffers are sized as SpLSQR's, for the same reason.
estCap := min(maxIter, 2*n)
rEsts := make([]float64, 0, estCap)
arEsts := make([]float64, 0, estCap)
normA := math.Sqrt(normA2)
condA := 1.0
normr := bNorm
hScale := max(alpha, bNorm)
// The rotation's cosine and sine live across the loop body; ρ
// itself carries across iterations, which is what rhoold reads.
var c, s float64
// hitBudget tells the three exits apart, exactly as SpLSQR's: a
// loop that ended by its own iteration count fired no criterion
// and broke down nowhere, which is the budget's fault.
hitBudget := true
for itn := 1; itn <= maxIter; itn++ {
// βu = A·v − αu, αv = Aᵀu − βv.
op.matVec(v, uBuf)
for i := range m {
uBuf[i] -= alpha * u[i]
}
if !vecFinite(uBuf) {
return nil, nil, base.Errf("%s: non-finite residual direction at step %d", name, itn)
}
beta := norm2F64(uBuf)
hScale = max(hScale, beta)
floor := spLeastSquaresBreakdown * base.EpsF * hScale
if beta <= floor {
beta = 0
exhausted = true
} else {
scale = 1 / beta
for i := range m {
u[i] = uBuf[i] * scale
}
op.tMatVec(u, vBuf)
for j := range n {
vBuf[j] -= beta * v[j]
}
if !vecFinite(vBuf) {
return nil, nil, base.Errf("%s: non-finite normal direction at step %d", name, itn)
}
alpha = norm2F64(vBuf)
hScale = max(hScale, alpha)
if alpha <= floor {
// The v-direction is gone: α is treated as the exact
// zero the recursion would have produced, v as the zero
// vector, and the iteration stops after this fold.
alpha = 0
exhausted = true
clear(v)
} else if alpha > 0 {
scale = 1 / alpha
for j := range n {
v[j] = vBuf[j] * scale
}
}
}
// First rotation pair: the damping fold first, as the reference
// applies it: (ᾱ, 0) gives the sign chat and the magnitude α̂,
// then (α̂, β) turns to ρ. c and s live in the loop, but ρ is
// declared outside it and carries: rhoold needs the previous
// step's value, and a `:=` here would shadow it.
chat := sign(alphabar)
rhoold := rho
c, s, rho = symOrtho(math.Abs(alphabar), beta)
if rho == 0 {
exhausted = true
hitBudget = false
break
}
thetanew := s * alpha
alphabar = c * alpha
// Second rotation pair: (c̄ρ, θ) to ρ̄, which moves the
// normal-equations estimate ζ̄.
rhobarold := rhobar
zetaold := zeta
thetabar := sbar * rho
rhotemp := cbar * rho
cbar, sbar, rhobar = symOrtho(cbar*rho, thetanew)
zeta = cbar * zetabar
zetabar = -sbar * zetabar
// The direction recurrences and the answer update.
coef := thetabar * rho / (rhoold * rhobarold)
for j := range n {
hbar[j] = h[j] - coef*hbar[j]
}
xk := zeta / (rho * rhobar)
for j := range n {
x[j] += xk * hbar[j]
}
ht := -thetanew / rho
for j := range n {
h[j] = v[j] + ht*h[j]
}
steps = itn
// The exact-form residual estimate: the pending right-hand side
// entries ride the rotations, chat carrying the sign the
// damping fold produced.
betaacute := chat * betadd
betahat := c * betaacute
betadd = -s * betaacute
thetatildeold := thetatilde
ctildeold, stildeold, rhotildeold := symOrtho(rhodold, thetabar)
thetatilde = stildeold * rhobar
rhodold = ctildeold * rhobar
betad = -stildeold*betad + ctildeold*betahat
tautildeold = (zetaold - thetatildeold*tautildeold) / rhotildeold
taud := (zeta - thetatilde*tautildeold) / rhodold
normr = math.Sqrt((betad-taud)*(betad-taud) + betadd*betadd)
// The ‖A‖ and cond(A) estimates.
normA2 += beta * beta
normA = math.Sqrt(normA2)
normA2 += alpha * alpha
maxrbar = max(maxrbar, rhobarold)
if itn > 1 {
minrbar = min(minrbar, rhobarold)
}
condA = math.Inf(1)
if d := min(minrbar, rhotemp); d > 0 {
condA = max(maxrbar, rhotemp) / d
}
normar := math.Abs(zetabar)
normx := norm2F64(x)
rEsts = append(rEsts, normr)
arEsts = append(arEsts, normar)
test1 := normr / bNorm
test2 := math.Inf(1)
if p := normA * normr; p != 0 {
test2 = normar / p
}
test3 := 1 / (condA + base.EpsF)
target := tol * (1 + normA*normx/bNorm)
switch {
case test1 <= target:
criterion = LeastSquaresResidual
case test2 <= tol:
criterion = LeastSquaresNormal
case test3 <= ctol:
criterion = LeastSquaresCondition
}
if criterion != "" || exhausted {
hitBudget = false
break
}
}
rNorm, arNorm := lsqrExplicitNorms(op, bv, x, uBuf, vBuf)
criterion = settleLeastSquares(criterion, exhausted, rNorm, arNorm, x, bNorm, normA, tol)
if criterion == "" {
if exhausted {
return nil, nil, base.Errf("%s: no convergence: the recursion folded at step %d, residual %.3g (tolerance %.3g)",
name, steps, rNorm, tol)
}
if hitBudget {
return nil, nil, base.Errf("%s: no convergence in %d steps, residual %.3g (tolerance %.3g)",
name, maxIter, rNorm, tol)
}
return nil, nil, base.Errf("%s: a stopping criterion fired at step %d but its estimate drifted past the recomputed norms (residual %.3g, tolerance %.3g)",
name, steps, rNorm, tol)
}
info.Iterations = steps
info.Criterion = criterion
info.Converged = criterion != LeastSquaresCondition
info.ResidualNorm = rNorm
info.NormalResidual = arNorm
info.MatrixNorm = normA
info.Condition = condA
info.residualEstimates = rEsts
info.normalEstimates = arEsts
return floatsToArray(x, []int{n}), info, nil
}
// finishLeastSquares recomputes the explicit norms of a candidate x
// and hands the verdict to settleLeastSquares, the entry for a caller
// that holds only the operator and the vectors. The solvers call
// settleLeastSquares directly with the norms they already hold, so a
// solve never runs those two products twice.
func finishLeastSquares(name string, criterion string, exhausted bool, steps int, op *lsqrOperator, bv, x []float64, bNorm, matrixNorm, tol float64) string {
ax := make([]float64, op.m)
ar := make([]float64, op.n)
rNorm, arNorm := lsqrExplicitNorms(op, bv, x, ax, ar)
return settleLeastSquares(criterion, exhausted, rNorm, arNorm, x, bNorm, matrixNorm, tol)
}
// settleLeastSquares settles the criterion against the truth. A fired
// criterion is verified against the explicitly recomputed residual,
// with the round-off slack, so a drifted estimate cannot dress a
// broken answer up as converged; a collapse that fired no test is
// judged by its explicit norms the same way. It returns the criterion
// to report, or the empty string when the honest answer is an error.
func settleLeastSquares(criterion string, exhausted bool, rNorm, arNorm float64, x []float64, bNorm, matrixNorm, tol float64) string {
if criterion == "" && exhausted {
// The recursion clamped to a stop without a test firing: judge
// the folded answer by its explicit norms.
criterion = LeastSquaresResidual
}
if criterion == "" {
return ""
}
final, ok := verifyLeastSquares(criterion, rNorm, arNorm, bNorm, matrixNorm, norm2F64(x), tol)
if !ok {
return ""
}
return final
}