Files

808 lines
28 KiB
Go
Raw Permalink Normal View History

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
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
}