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