469 lines
16 KiB
Go
469 lines
16 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
|
|
// SPDX-License-Identifier: MIT
|
|||
|
|
|
|||
|
|
package linalg
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"math"
|
|||
|
|
"strings"
|
|||
|
|
"testing"
|
|||
|
|
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor/internal/core"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// overdeterminedLSFixture builds a deterministic sparse overdetermined
|
|||
|
|
// system: thirty rows, twelve columns, three stored entries per row,
|
|||
|
|
// a right-hand side taken from a known x through the matrix plus a
|
|||
|
|
// small inconsistent part, and its dense twin for the reference
|
|||
|
|
// solvers.
|
|||
|
|
func overdeterminedLSFixture(t *testing.T) (coo *core.SparseCOO, b *core.Array, dense *core.Array) {
|
|||
|
|
t.Helper()
|
|||
|
|
const m, n = 30, 12
|
|||
|
|
g := core.NewGenerator(7)
|
|||
|
|
idx := make([]int64, 0, 3*m+2)
|
|||
|
|
vals := make([]float64, 0, 3*m+2)
|
|||
|
|
add := func(r, c int, v float64) {
|
|||
|
|
idx = append(idx, int64(r), int64(c))
|
|||
|
|
vals = append(vals, v)
|
|||
|
|
}
|
|||
|
|
for i := range m {
|
|||
|
|
add(i, i%n, 1+float64(g.Next()%100)/200)
|
|||
|
|
add(i, (i*7+3)%n, -1+float64(g.Next()%100)/100)
|
|||
|
|
add(i, (i*13+5)%n, float64(g.Next()%100)/100)
|
|||
|
|
}
|
|||
|
|
add(0, 0, 3)
|
|||
|
|
add(1, 2, 2)
|
|||
|
|
indices, err := core.FromInts(idx, len(vals), 2)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("FromInts: %v", err)
|
|||
|
|
}
|
|||
|
|
coo, err = core.NewSparseCOO(indices, floatsToArray(vals, []int{len(vals)}), []int{m, n})
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("NewSparseCOO: %v", err)
|
|||
|
|
}
|
|||
|
|
csr, err := CSRFromCOO(coo)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("CSRFromCOO: %v", err)
|
|||
|
|
}
|
|||
|
|
xTrue := make([]float64, n)
|
|||
|
|
for i := range n {
|
|||
|
|
xTrue[i] = math.Sin(0.7*float64(i)) + float64(i%5)*0.3 - 0.6
|
|||
|
|
}
|
|||
|
|
b, err = csr.MatVec(floatsToArray(xTrue, []int{n}))
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("MatVec: %v", err)
|
|||
|
|
}
|
|||
|
|
for i := range m {
|
|||
|
|
b.RawFloats()[i] += 0.01 * math.Cos(float64(i))
|
|||
|
|
}
|
|||
|
|
denseVals := make([]float64, m*n)
|
|||
|
|
for i := range len(vals) {
|
|||
|
|
denseVals[int(idx[2*i])*n+int(idx[2*i+1])] += vals[i]
|
|||
|
|
}
|
|||
|
|
return coo, b, floatsToArray(denseVals, []int{m, n})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// lsAchievedNorms recomputes ‖b − A·x‖ and ‖Aᵀ(b − A·x)‖ through the
|
|||
|
|
// public sparse surface, independently of the solver's own operator.
|
|||
|
|
func lsAchievedNorms(t *testing.T, coo *core.SparseCOO, b, x *core.Array) (rNorm, arNorm float64) {
|
|||
|
|
t.Helper()
|
|||
|
|
csr, err := CSRFromCOO(coo)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("CSRFromCOO: %v", err)
|
|||
|
|
}
|
|||
|
|
ax, err := csr.MatVec(x)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("MatVec: %v", err)
|
|||
|
|
}
|
|||
|
|
r := core.New(core.Float, b.Len())
|
|||
|
|
for i := range b.Len() {
|
|||
|
|
r.RawFloats()[i] = b.FloatAt(i) - ax.FloatAt(i)
|
|||
|
|
}
|
|||
|
|
rNorm = norm2F64(vectorF64(r, b.Len()))
|
|||
|
|
at, err := csr.Transpose().MatVec(r)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("MatVec: %v", err)
|
|||
|
|
}
|
|||
|
|
arNorm = norm2F64(vectorF64(at, at.Len()))
|
|||
|
|
return rNorm, arNorm
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestSpLSQRMatchesDenseLeastSquares(t *testing.T) {
|
|||
|
|
coo, b, dense := overdeterminedLSFixture(t)
|
|||
|
|
ref, err := LeastSquares(dense, b)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("LeastSquares: %v", err)
|
|||
|
|
}
|
|||
|
|
x, info, err := SpLSQR(coo, b, 1e-12, 0, 0)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("SpLSQR: %v", err)
|
|||
|
|
}
|
|||
|
|
worst, refMax := 0.0, 0.0
|
|||
|
|
for i := range ref.Len() {
|
|||
|
|
if d := math.Abs(x.FloatAt(i) - ref.FloatAt(i)); d > worst {
|
|||
|
|
worst = d
|
|||
|
|
}
|
|||
|
|
if v := math.Abs(ref.FloatAt(i)); v > refMax {
|
|||
|
|
refMax = v
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if worst > 1e-9*refMax {
|
|||
|
|
t.Fatalf("LSQR disagrees with the dense solve by %.3g (scale %.3g)", worst, refMax)
|
|||
|
|
}
|
|||
|
|
if !info.Converged {
|
|||
|
|
t.Fatalf("LSQR stopped without convergence: %+v", info)
|
|||
|
|
}
|
|||
|
|
if info.Criterion != LeastSquaresResidual && info.Criterion != LeastSquaresNormal {
|
|||
|
|
t.Fatalf("criterion %q is not a residual test", info.Criterion)
|
|||
|
|
}
|
|||
|
|
if info.Iterations > 2*12 {
|
|||
|
|
t.Fatalf("LSQR took %d steps for a 12-column Krylov space", info.Iterations)
|
|||
|
|
}
|
|||
|
|
// The achieved quantities must be the explicit truth, not the
|
|||
|
|
// in-loop estimates.
|
|||
|
|
rNorm, arNorm := lsAchievedNorms(t, coo, b, x)
|
|||
|
|
if math.Abs(rNorm-info.ResidualNorm) > 1e-9*info.ResidualNorm {
|
|||
|
|
t.Fatalf("reported residual %.3g does not match the explicit %.3g", info.ResidualNorm, rNorm)
|
|||
|
|
}
|
|||
|
|
if math.Abs(arNorm-info.NormalResidual) > 1e-9*info.NormalResidual {
|
|||
|
|
t.Fatalf("reported normal residual %.3g does not match the explicit %.3g", info.NormalResidual, arNorm)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestSpLSMRMatchesDenseLeastSquares(t *testing.T) {
|
|||
|
|
coo, b, dense := overdeterminedLSFixture(t)
|
|||
|
|
ref, err := LeastSquares(dense, b)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("LeastSquares: %v", err)
|
|||
|
|
}
|
|||
|
|
x, info, err := SpLSMR(coo, b, 1e-12, 0, 0)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("SpLSMR: %v", err)
|
|||
|
|
}
|
|||
|
|
worst, refMax := 0.0, 0.0
|
|||
|
|
for i := range ref.Len() {
|
|||
|
|
if d := math.Abs(x.FloatAt(i) - ref.FloatAt(i)); d > worst {
|
|||
|
|
worst = d
|
|||
|
|
}
|
|||
|
|
if v := math.Abs(ref.FloatAt(i)); v > refMax {
|
|||
|
|
refMax = v
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if worst > 1e-9*refMax {
|
|||
|
|
t.Fatalf("LSMR disagrees with the dense solve by %.3g (scale %.3g)", worst, refMax)
|
|||
|
|
}
|
|||
|
|
if !info.Converged || info.Criterion == "" {
|
|||
|
|
t.Fatalf("LSMR stopped without convergence: %+v", info)
|
|||
|
|
}
|
|||
|
|
// LSMR's own minimisation target moves monotonically.
|
|||
|
|
for i := 1; i < len(info.normalEstimates); i++ {
|
|||
|
|
if info.normalEstimates[i] > info.normalEstimates[i-1] {
|
|||
|
|
t.Fatalf("LSMR normal-equations estimate rose at step %d: %.6g after %.6g",
|
|||
|
|
i, info.normalEstimates[i], info.normalEstimates[i-1])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestSpLSQREstimateTrajectoriesMonotone(t *testing.T) {
|
|||
|
|
coo, b, _ := overdeterminedLSFixture(t)
|
|||
|
|
// LSQR's residual estimate is |φ̄|, shrank every step by |sn| ≤ 1;
|
|||
|
|
// LSMR's normal estimate is |ζ̄|, shrank by |s̄| ≤ 1. Both are
|
|||
|
|
// monotone by construction and must stay so in float.
|
|||
|
|
_, lsInfo, err := SpLSQR(coo, b, 1e-12, 0, 0)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("SpLSQR: %v", err)
|
|||
|
|
}
|
|||
|
|
for i := 1; i < len(lsInfo.residualEstimates); i++ {
|
|||
|
|
if lsInfo.residualEstimates[i] > lsInfo.residualEstimates[i-1] {
|
|||
|
|
t.Fatalf("LSQR residual estimate rose at step %d: %.6g after %.6g",
|
|||
|
|
i, lsInfo.residualEstimates[i], lsInfo.residualEstimates[i-1])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
_, lsmrInfo, err := SpLSMR(coo, b, 1e-12, 0, 0)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("SpLSMR: %v", err)
|
|||
|
|
}
|
|||
|
|
for i := 1; i < len(lsmrInfo.normalEstimates); i++ {
|
|||
|
|
if lsmrInfo.normalEstimates[i] > lsmrInfo.normalEstimates[i-1] {
|
|||
|
|
t.Fatalf("LSMR normal estimate rose at step %d: %.6g after %.6g",
|
|||
|
|
i, lsmrInfo.normalEstimates[i], lsmrInfo.normalEstimates[i-1])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestSpLeastSquaresMinimumNorm pins the hand-checkable rank-deficient
|
|||
|
|
// consistent system A = [[1,0,1],[0,1,1],[1,1,2]], b = (1,2,3): the
|
|||
|
|
// solution family is (1−t, 2−t, t) and the minimum-norm member is
|
|||
|
|
// (0, 1, 1), which both solvers must answer from a zero start.
|
|||
|
|
func TestSpLeastSquaresMinimumNorm(t *testing.T) {
|
|||
|
|
indices, err := core.FromInts([]int64{0, 0, 0, 2, 1, 1, 1, 2, 2, 0, 2, 1, 2, 2}, 7, 2)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("FromInts: %v", err)
|
|||
|
|
}
|
|||
|
|
coo, err := core.NewSparseCOO(indices, floatsToArray([]float64{1, 1, 1, 1, 1, 1, 2}, []int{7}), []int{3, 3})
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("NewSparseCOO: %v", err)
|
|||
|
|
}
|
|||
|
|
csr, err := CSRFromCOO(coo)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("CSRFromCOO: %v", err)
|
|||
|
|
}
|
|||
|
|
b, err := csr.MatVec(floatsToArray([]float64{1, 2, 0}, []int{3}))
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("MatVec: %v", err)
|
|||
|
|
}
|
|||
|
|
want := []float64{0, 1, 1}
|
|||
|
|
for name, run := range map[string]func(*core.SparseCOO, *core.Array) (*core.Array, *LeastSquaresInfo, error){
|
|||
|
|
"LSQR": func(a *core.SparseCOO, bb *core.Array) (*core.Array, *LeastSquaresInfo, error) {
|
|||
|
|
return SpLSQR(a, bb, 1e-13, 0, 0)
|
|||
|
|
},
|
|||
|
|
"LSMR": func(a *core.SparseCOO, bb *core.Array) (*core.Array, *LeastSquaresInfo, error) {
|
|||
|
|
return SpLSMR(a, bb, 1e-13, 0, 0)
|
|||
|
|
},
|
|||
|
|
} {
|
|||
|
|
x, info, err := run(coo, b)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("%s: %v", name, err)
|
|||
|
|
}
|
|||
|
|
for i := range want {
|
|||
|
|
if math.Abs(x.FloatAt(i)-want[i]) > 1e-10 {
|
|||
|
|
t.Fatalf("%s: x[%d] = %.12g, want the minimum-norm %.12g", name, i, x.FloatAt(i), want[i])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if !info.Converged {
|
|||
|
|
t.Fatalf("%s: not converged: %+v", name, info)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// The same answer must agree with the SVD's minimum-norm solution.
|
|||
|
|
dense := floatsToArray([]float64{1, 0, 1, 0, 1, 1, 1, 1, 2}, []int{3, 3})
|
|||
|
|
pinv, err := Pinverse(dense, 0)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("Pinverse: %v", err)
|
|||
|
|
}
|
|||
|
|
ref := core.New(core.Float, 3)
|
|||
|
|
for i := range 3 {
|
|||
|
|
s := 0.0
|
|||
|
|
for j := range 3 {
|
|||
|
|
s += pinv.FloatAt(i*3+j) * b.FloatAt(j)
|
|||
|
|
}
|
|||
|
|
ref.RawFloats()[i] = s
|
|||
|
|
}
|
|||
|
|
for i := range 3 {
|
|||
|
|
if math.Abs(ref.FloatAt(i)-want[i]) > 1e-12 {
|
|||
|
|
t.Fatalf("Pinverse reference %.12g disagrees with the hand solution %.12g", ref.FloatAt(i), want[i])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestSpLeastSquaresIllConditioned pins convergence with the criterion
|
|||
|
|
// recorded on a system whose diagonal decays by four orders: the
|
|||
|
|
// conditional estimate stays under the limit and a residual test
|
|||
|
|
// ends the iteration.
|
|||
|
|
func TestSpLeastSquaresIllConditioned(t *testing.T) {
|
|||
|
|
const n = 10
|
|||
|
|
idx := make([]int64, 0, 3*n)
|
|||
|
|
vals := make([]float64, 0, 3*n)
|
|||
|
|
for i := range n {
|
|||
|
|
idx = append(idx, int64(i), int64(i))
|
|||
|
|
vals = append(vals, math.Pow(10, -0.45*float64(i)))
|
|||
|
|
if i+1 < n {
|
|||
|
|
idx = append(idx, int64(i), int64(i+1), int64(i+1), int64(i))
|
|||
|
|
vals = append(vals, 1e-7, 1e-7)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
indices, err := core.FromInts(idx, len(vals), 2)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("FromInts: %v", err)
|
|||
|
|
}
|
|||
|
|
coo, err := core.NewSparseCOO(indices, floatsToArray(vals, []int{len(vals)}), []int{n, n})
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("NewSparseCOO: %v", err)
|
|||
|
|
}
|
|||
|
|
csr, err := CSRFromCOO(coo)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("CSRFromCOO: %v", err)
|
|||
|
|
}
|
|||
|
|
b, err := csr.MatVec(floatsToArray([]float64{1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, []int{n}))
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("MatVec: %v", err)
|
|||
|
|
}
|
|||
|
|
for name, run := range map[string]func(*core.SparseCOO, *core.Array) (*core.Array, *LeastSquaresInfo, error){
|
|||
|
|
"LSQR": func(a *core.SparseCOO, bb *core.Array) (*core.Array, *LeastSquaresInfo, error) {
|
|||
|
|
return SpLSQR(a, bb, 1e-9, 0, 0)
|
|||
|
|
},
|
|||
|
|
"LSMR": func(a *core.SparseCOO, bb *core.Array) (*core.Array, *LeastSquaresInfo, error) {
|
|||
|
|
return SpLSMR(a, bb, 1e-9, 0, 0)
|
|||
|
|
},
|
|||
|
|
} {
|
|||
|
|
x, info, err := run(coo, b)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("%s: %v", name, err)
|
|||
|
|
}
|
|||
|
|
if !info.Converged || info.Criterion == "" {
|
|||
|
|
t.Fatalf("%s: ill-conditioned system ended without a recorded criterion: %+v", name, info)
|
|||
|
|
}
|
|||
|
|
if info.Condition <= 0 || info.MatrixNorm <= 0 {
|
|||
|
|
t.Fatalf("%s: estimates not reported: %+v", name, info)
|
|||
|
|
}
|
|||
|
|
rNorm, _ := lsAchievedNorms(t, coo, b, x)
|
|||
|
|
if rNorm > 1e-8 {
|
|||
|
|
t.Fatalf("%s: achieved residual %.3g is too coarse", name, rNorm)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestSpLeastSquaresErrors(t *testing.T) {
|
|||
|
|
coo, b, _ := overdeterminedLSFixture(t)
|
|||
|
|
// Underdetermined systems are refused, in the dense surface's own
|
|||
|
|
// words.
|
|||
|
|
small, err := core.NewSparseCOO(
|
|||
|
|
mustInts(t, []int64{0, 0, 0, 1}, 2, 2),
|
|||
|
|
floatsToArray([]float64{1, 1}, []int{2}),
|
|||
|
|
[]int{2, 3})
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("NewSparseCOO: %v", err)
|
|||
|
|
}
|
|||
|
|
for name, run := range map[string]func(*core.SparseCOO, *core.Array) (*core.Array, *LeastSquaresInfo, error){
|
|||
|
|
"LSQR": func(a *core.SparseCOO, bb *core.Array) (*core.Array, *LeastSquaresInfo, error) {
|
|||
|
|
return SpLSQR(a, bb, 0, 0, 0)
|
|||
|
|
},
|
|||
|
|
"LSMR": func(a *core.SparseCOO, bb *core.Array) (*core.Array, *LeastSquaresInfo, error) {
|
|||
|
|
return SpLSMR(a, bb, 0, 0, 0)
|
|||
|
|
},
|
|||
|
|
} {
|
|||
|
|
if _, _, err := run(small, mustFloats(t, []float64{1, 2}, 2)); err == nil || !strings.Contains(err.Error(), "overdetermined") {
|
|||
|
|
t.Fatalf("%s: underdetermined system accepted: %v", name, err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Complex inputs.
|
|||
|
|
complexValues, err := core.FromComplexes([]complex128{1}, 1)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("FromComplexes: %v", err)
|
|||
|
|
}
|
|||
|
|
complexCOO, err := core.NewSparseCOO(mustInts(t, []int64{0, 0}, 1, 2), complexValues, []int{1, 1})
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("NewSparseCOO: %v", err)
|
|||
|
|
}
|
|||
|
|
if _, _, err := SpLSQR(complexCOO, mustFloats(t, []float64{1}, 1), 0, 0, 0); err == nil {
|
|||
|
|
t.Fatal("a complex matrix was accepted")
|
|||
|
|
}
|
|||
|
|
if _, _, err := SpLSQR(coo, mustFloats(t, []float64{1, 2}, 2), 0, 0, 0); err == nil {
|
|||
|
|
t.Fatal("a short right-hand side was accepted")
|
|||
|
|
}
|
|||
|
|
if _, _, err := SpLSQR(coo, core.New(core.Float, 3, 3), 0, 0, 0); err == nil {
|
|||
|
|
t.Fatal("a rank-2 right-hand side was accepted")
|
|||
|
|
}
|
|||
|
|
// A budget that runs out with every tolerance unmet is the budget's
|
|||
|
|
// own fault: the error names the steps, not a drift no estimate
|
|||
|
|
// committed. A tolerance of 1e-14 with two steps fires no criterion
|
|||
|
|
// (the residual is still 1.59), and neither does 1e-300 in one.
|
|||
|
|
if _, _, err := SpLSQR(coo, b, 1e-14, 2, 0); err == nil ||
|
|||
|
|
!strings.Contains(err.Error(), "no convergence in 2 steps") ||
|
|||
|
|
strings.Contains(err.Error(), "drifted") {
|
|||
|
|
t.Fatalf("exhausted budget misreported: %v", err)
|
|||
|
|
}
|
|||
|
|
if _, _, err := SpLSQR(coo, b, 1e-300, 1, 0); err == nil ||
|
|||
|
|
!strings.Contains(err.Error(), "no convergence in 1 steps") ||
|
|||
|
|
strings.Contains(err.Error(), "drifted") {
|
|||
|
|
t.Fatalf("exhausted budget misreported: %v", err)
|
|||
|
|
}
|
|||
|
|
if _, _, err := SpLSMR(coo, b, 1e-300, 1, 0); err == nil ||
|
|||
|
|
!strings.Contains(err.Error(), "no convergence in 1 steps") ||
|
|||
|
|
strings.Contains(err.Error(), "drifted") {
|
|||
|
|
t.Fatalf("exhausted budget misreported: %v", err)
|
|||
|
|
}
|
|||
|
|
// A zero right-hand side answers the exact zero without a step.
|
|||
|
|
zero := floatsToArray(make([]float64, b.Len()), []int{b.Len()})
|
|||
|
|
x, info, err := SpLSQR(coo, zero, 0, 0, 0)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("zero right-hand side: %v", err)
|
|||
|
|
}
|
|||
|
|
for i := range x.Len() {
|
|||
|
|
if x.FloatAt(i) != 0 {
|
|||
|
|
t.Fatalf("zero right-hand side answered %.3g", x.FloatAt(i))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if info.Criterion != "" || !info.Converged || info.Iterations != 0 {
|
|||
|
|
t.Fatalf("zero right-hand side info: %+v", info)
|
|||
|
|
}
|
|||
|
|
// A non-empty matrix with no column-space component of b answers
|
|||
|
|
// the exact zero as well: both columns lie along (1,0) and
|
|||
|
|
// b = (0,1) is orthogonal to the column space.
|
|||
|
|
null, err := core.NewSparseCOO(mustInts(t, []int64{0, 0, 0, 1}, 2, 2), floatsToArray([]float64{1, 2}, []int{2}), []int{2, 2})
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("NewSparseCOO: %v", err)
|
|||
|
|
}
|
|||
|
|
ortho := mustFloats(t, []float64{0, 1}, 2)
|
|||
|
|
x, info, err = SpLSQR(null, ortho, 0, 0, 0)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("orthogonal right-hand side: %v", err)
|
|||
|
|
}
|
|||
|
|
for i := range x.Len() {
|
|||
|
|
if x.FloatAt(i) != 0 {
|
|||
|
|
t.Fatalf("orthogonal right-hand side answered %.3g", x.FloatAt(i))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if info.ResidualNorm <= 0 {
|
|||
|
|
t.Fatalf("the achieved residual of an orthogonal system vanished: %+v", info)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestFinishLeastSquaresDriftRefusal pins the drift guard at its own
|
|||
|
|
// gate: a fired criterion whose recomputed norms do not carry it is
|
|||
|
|
// refused with the empty string, while the same answer at a tolerance
|
|||
|
|
// it genuinely meets is carried. The end-to-end budget pins live in
|
|||
|
|
// TestSpLeastSquaresErrors; the drift itself needs the estimate and
|
|||
|
|
// the truth to disagree, which is settled here without a recursion.
|
|||
|
|
func TestFinishLeastSquaresDriftRefusal(t *testing.T) {
|
|||
|
|
coo, err := core.NewSparseCOO(mustInts(t, []int64{0, 0}, 1, 2), mustFloats(t, []float64{1}, 1), []int{1, 1})
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("NewSparseCOO: %v", err)
|
|||
|
|
}
|
|||
|
|
op, err := newLSQROperator(coo)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("newLSQROperator: %v", err)
|
|||
|
|
}
|
|||
|
|
// x = 0.5 against A = [1], b = [1] leaves both the residual and
|
|||
|
|
// the normal residual at 0.5 with ‖b‖ = 1.
|
|||
|
|
if got := finishLeastSquares("SpLSQR", LeastSquaresResidual, false, 3, op, []float64{1}, []float64{0.5}, 1, 1, 1e-14); got != "" {
|
|||
|
|
t.Fatalf("a drifted criterion carried as %q", got)
|
|||
|
|
}
|
|||
|
|
if got := finishLeastSquares("SpLSQR", LeastSquaresResidual, false, 3, op, []float64{1}, []float64{0.5}, 1, 1, 0.4); got != LeastSquaresResidual {
|
|||
|
|
t.Fatalf("an honestly met criterion refused: %q", got)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestSpLSQREstimateSeriesIdentities pins what each estimate series
|
|||
|
|
// carries, not merely that it shrinks. LSQR's residual estimate tracks
|
|||
|
|
// ‖b − A·x‖ and its normal-equations estimate tracks ‖Aᵀ(b − A·x)‖, so
|
|||
|
|
// on a converged fit the first ends beside the explicitly recomputed
|
|||
|
|
// info.ResidualNorm while the second sits orders of magnitude below it.
|
|||
|
|
// Filling the residual series with the normal estimate, or the reverse,
|
|||
|
|
// keeps both series non-increasing and fails here.
|
|||
|
|
func TestSpLSQREstimateSeriesIdentities(t *testing.T) {
|
|||
|
|
coo, b, _ := overdeterminedLSFixture(t)
|
|||
|
|
_, info, err := SpLSQR(coo, b, 1e-12, 0, 0)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("SpLSQR: %v", err)
|
|||
|
|
}
|
|||
|
|
re, ne := info.residualEstimates, info.normalEstimates
|
|||
|
|
if len(re) != info.Iterations || len(ne) != info.Iterations {
|
|||
|
|
t.Fatalf("estimate series of %d and %d against %d recorded steps", len(re), len(ne), info.Iterations)
|
|||
|
|
}
|
|||
|
|
if len(re) == 0 {
|
|||
|
|
t.Fatal("no estimates recorded")
|
|||
|
|
}
|
|||
|
|
lastRe, lastNe := re[len(re)-1], ne[len(ne)-1]
|
|||
|
|
if dev := math.Abs(lastRe - info.ResidualNorm); dev > 0.5*info.ResidualNorm {
|
|||
|
|
t.Fatalf("the residual estimate ends at %.6g against the explicit residual %.6g, want the two on the same scale",
|
|||
|
|
lastRe, info.ResidualNorm)
|
|||
|
|
}
|
|||
|
|
if lastNe > 1e-3*info.ResidualNorm {
|
|||
|
|
t.Fatalf("the normal-equations estimate ends at %.6g against a residual of %.6g, want the normal residual's own, much smaller scale",
|
|||
|
|
lastNe, info.ResidualNorm)
|
|||
|
|
}
|
|||
|
|
if lastRe == lastNe {
|
|||
|
|
t.Fatalf("both series end at %.6g, want the residual and the normal-equations estimates", lastRe)
|
|||
|
|
}
|
|||
|
|
}
|