Files
tensor/linalg/solver_guard_pins_test.go
T

136 lines
4.3 KiB
Go
Raw Normal View History

2026-09-03 10:00:00 +02:00
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package linalg
import (
"math"
"strings"
"testing"
"sourcedock.dev/petrbalvin/tensor/internal/core"
)
// Regression pins: NaN states that read as converged solves, and
// contract gaps the sibling solvers had already closed.
func cooFrom(t *testing.T, idx []int64, vals []float64, shape []int) *core.SparseCOO {
t.Helper()
i, err := core.FromInts(idx, len(idx)/len(shape), len(shape))
if err != nil {
t.Fatalf("FromInts: %v", err)
}
v, err := core.FromFloats(vals, len(vals))
if err != nil {
t.Fatalf("FromFloats: %v", err)
}
sp, err := core.NewSparseCOO(i, v, shape)
if err != nil {
t.Fatalf("NewSparseCOO: %v", err)
}
return sp
}
// TestSpSolveOverflowCurvature: with every input finite, the Jacobi
// preconditioner overflowed the curvature to +Inf, Inf/Inf gave a NaN
// alpha, and the all-NaN residual read as a converged solve through
// the NaN-skipping norm.
func TestSpSolveOverflowCurvature(t *testing.T) {
sp := cooFrom(t, []int64{0, 0, 1, 1}, []float64{1e-155, 1e-155}, []int{2, 2})
b, err := core.FromFloats([]float64{1e155, 1e155}, 2)
if err != nil {
t.Fatal(err)
}
x, err := SpSolve(sp, b, 0, 0)
if err == nil {
t.Fatalf("an overflowed solve returned %v with no error", x.FloatAt(0))
}
if x != nil {
t.Fatalf("SpSolve returned a result beside the error")
}
}
// TestSpSolveBiCGSTABOverflowOmega: a tiny diagonal entry overflowed
// the preconditioned stabiliser to Inf and its dots to NaN/Inf, omega
// became NaN, and the finiteness guard sat behind the convergence
// return it was written to protect.
func TestSpSolveBiCGSTABOverflowOmega(t *testing.T) {
// A = [[1e-155, 1], [1, 1]]: the Jacobi preconditioner divides the
// first residual by 1e-155 and the stabiliser leg overflows.
sp := cooFrom(t, []int64{0, 0, 0, 1, 1, 0, 1, 1},
[]float64{1e-155, 1, 1, 1}, []int{2, 2})
b, err := core.FromFloats([]float64{1, 0}, 2)
if err != nil {
t.Fatal(err)
}
if _, err := SpSolveBiCGSTAB(sp, b, 0, 0); err == nil {
t.Fatal("an overflowed stabiliser returned a solution with no error")
}
}
// TestGMRESRejectsRank2: every sibling entry point refuses a non-vector
// right-hand side; GMRES flattened it silently.
func TestGMRESRejectsRank2(t *testing.T) {
b, err := core.FromFloats([]float64{1, 2, 3, 4}, 2, 2)
if err != nil {
t.Fatal(err)
}
ident := func(v *core.Array) (*core.Array, error) { return core.Copy(v), nil }
if _, err := GMRES(ident, b, 0, 0, 0); err == nil || !strings.Contains(err.Error(), "rank-1") {
t.Fatalf("GMRES on a rank-2 b: err = %v", err)
}
}
// TestGMRESRejectsNonFiniteOp: an operator answering NaN must be an
// error, not a zero-norm residual that reads as convergence.
func TestGMRESRejectsNonFiniteOp(t *testing.T) {
b, err := core.FromFloats([]float64{1}, 1)
if err != nil {
t.Fatal(err)
}
nan := math.NaN()
broken := func(v *core.Array) (*core.Array, error) { return core.FromFloats([]float64{nan}, 1) }
if _, err := GMRES(broken, b, 0, 0, 0); err == nil || !strings.Contains(err.Error(), "non-finite") {
t.Fatalf("GMRES with a NaN op: err = %v", err)
}
}
// TestGMRESTinyOperator: the breakdown floor was relative to ‖b‖, so a
// legitimate system with ‖A‖ ≪ ‖b‖ collapsed every cycle at its first
// column and ran out of cycles without converging.
func TestGMRESTinyOperator(t *testing.T) {
b, err := core.FromFloats([]float64{1, 1}, 2)
if err != nil {
t.Fatal(err)
}
scale := 1e-20
tiny := func(v *core.Array) (*core.Array, error) {
x := v.FloatAt(0) * scale
y := v.FloatAt(1) * scale
return core.FromFloats([]float64{x, y}, 2)
}
x, err := GMRES(tiny, b, 0, 0, 1e-8)
if err != nil {
t.Fatalf("GMRES on a tiny-norm operator: %v", err)
}
if math.Abs(x.FloatAt(0)-1/scale) > 1e-3/scale {
t.Fatalf("x[0] = %g, want %g", x.FloatAt(0), 1/scale)
}
}
// TestNewCubicSplineRejectsRank2: a matrix input was silently
// reinterpreted as a flattened vector.
func TestNewCubicSplineRejectsRank2(t *testing.T) {
xs, err := core.FromFloats([]float64{1, 2, 3, 4, 5, 6}, 2, 3)
if err != nil {
t.Fatal(err)
}
ys, err := core.FromFloats([]float64{1, 4, 9, 16, 25, 36}, 2, 3)
if err != nil {
t.Fatal(err)
}
if _, err := NewCubicSpline(xs, ys); err == nil || !strings.Contains(err.Error(), "1-D") {
t.Fatalf("NewCubicSpline on rank-2 inputs: err = %v", err)
}
}