808 lines
28 KiB
Go
808 lines
28 KiB
Go
// 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
|
||
}
|