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