// Copyright (c) 2026 Petr Balvín (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 }