536 lines
17 KiB
Go
536 lines
17 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: MIT
|
||
|
|
|
||
|
|
package linalg
|
||
|
|
|
||
|
|
import (
|
||
|
|
"math"
|
||
|
|
"slices"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"sourcedock.dev/petrbalvin/tensor/internal/core"
|
||
|
|
)
|
||
|
|
|
||
|
|
// gridLaplacianCOO builds the 5-point Laplacian on a w by h grid: the
|
||
|
|
// standard sparse positive definite test matrix, symmetric with a
|
||
|
|
// dominant diagonal. When shuffle is true the grid vertices are
|
||
|
|
// relabelled by a fixed seeded permutation first, so the stored order
|
||
|
|
// carries no bandwidth for the natural ordering to lean on.
|
||
|
|
func gridLaplacianCOOShuffled(t *testing.T, w, h int, shuffle bool) *core.SparseCOO {
|
||
|
|
t.Helper()
|
||
|
|
idx := make([]int64, 0, 5*w*h)
|
||
|
|
vals := make([]float64, 0, 5*w*h)
|
||
|
|
label := func(x, y int) int { return y*w + x }
|
||
|
|
if shuffle {
|
||
|
|
g := core.NewGenerator(5)
|
||
|
|
perm := make([]int, w*h)
|
||
|
|
for i := range perm {
|
||
|
|
perm[i] = i
|
||
|
|
}
|
||
|
|
// Fisher-Yates with the seeded generator, a fixed relabelling.
|
||
|
|
for i := len(perm) - 1; i > 0; i-- {
|
||
|
|
j := int(g.Next() % uint64(i+1))
|
||
|
|
perm[i], perm[j] = perm[j], perm[i]
|
||
|
|
}
|
||
|
|
label = func(x, y int) int { return perm[y*w+x] }
|
||
|
|
}
|
||
|
|
add := func(r, c int, v float64) {
|
||
|
|
idx = append(idx, int64(r), int64(c))
|
||
|
|
vals = append(vals, v)
|
||
|
|
}
|
||
|
|
at := func(x, y int) int { return label(x, y) }
|
||
|
|
for y := range h {
|
||
|
|
for x := range w {
|
||
|
|
add(at(x, y), at(x, y), 4)
|
||
|
|
if x+1 < w {
|
||
|
|
add(at(x, y), at(x+1, y), -1)
|
||
|
|
add(at(x+1, y), at(x, y), -1)
|
||
|
|
}
|
||
|
|
if y+1 < h {
|
||
|
|
add(at(x, y), at(x, y+1), -1)
|
||
|
|
add(at(x, y+1), at(x, y), -1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
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{w * h, w * h})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSparseCOO: %v", err)
|
||
|
|
}
|
||
|
|
return coo
|
||
|
|
}
|
||
|
|
|
||
|
|
// gridLaplacianCOO builds the plain row-major grid.
|
||
|
|
func gridLaplacianCOO(t *testing.T, w, h int) *core.SparseCOO {
|
||
|
|
t.Helper()
|
||
|
|
return gridLaplacianCOOShuffled(t, w, h, false)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSparseCholeskySolvesLaplacian(t *testing.T) {
|
||
|
|
const w, h = 12, 10
|
||
|
|
coo := gridLaplacianCOO(t, w, h)
|
||
|
|
n := w * h
|
||
|
|
csr, err := CSRFromCOO(coo)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("CSRFromCOO: %v", err)
|
||
|
|
}
|
||
|
|
// The truth is constructive: xTrue is fixed, b comes from the
|
||
|
|
// independently tested CSR MatVec, and the factor must invert it.
|
||
|
|
xTrue := core.New(core.Float, n)
|
||
|
|
for i := range n {
|
||
|
|
xTrue.RawFloats()[i] = math.Sin(float64(i)) + float64(i%7)*0.1
|
||
|
|
}
|
||
|
|
b, err := csr.MatVec(xTrue)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("MatVec: %v", err)
|
||
|
|
}
|
||
|
|
for _, ordering := range []SparseOrdering{SparseOrderingNatural, SparseOrderingReverseCuthillMcKee} {
|
||
|
|
f, err := NewSparseCholesky(coo, ordering)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSparseCholesky(%d): %v", ordering, err)
|
||
|
|
}
|
||
|
|
x, err := f.Solve(b)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Solve(%d): %v", ordering, err)
|
||
|
|
}
|
||
|
|
worst := 0.0
|
||
|
|
for i := range n {
|
||
|
|
if d := math.Abs(x.FloatAt(i) - xTrue.FloatAt(i)); d > worst {
|
||
|
|
worst = d
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if worst > 1e-9 {
|
||
|
|
t.Fatalf("ordering %d: worst solution error %.3g, want under 1e-9", ordering, worst)
|
||
|
|
}
|
||
|
|
// The residual through the original matrix closes the loop:
|
||
|
|
// A·x must give b back.
|
||
|
|
ax, err := csr.MatVec(x)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("MatVec: %v", err)
|
||
|
|
}
|
||
|
|
res := 0.0
|
||
|
|
for i := range n {
|
||
|
|
if d := math.Abs(ax.FloatAt(i) - b.FloatAt(i)); d > res {
|
||
|
|
res = d
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if res > 1e-8 {
|
||
|
|
t.Fatalf("ordering %d: residual %.3g, want under 1e-8", ordering, res)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// irregularSPDCOO builds a diagonally dominant symmetric matrix with
|
||
|
|
// long-range random couplings: the pattern no bandwidth ordering can
|
||
|
|
// tame, the case the minimum degree order exists for.
|
||
|
|
func irregularSPDCOO(t *testing.T, seed int64, n int) *core.SparseCOO {
|
||
|
|
t.Helper()
|
||
|
|
g := core.NewGenerator(seed)
|
||
|
|
deg := make([]float64, n)
|
||
|
|
type entry struct {
|
||
|
|
r, c int
|
||
|
|
}
|
||
|
|
var entries []entry
|
||
|
|
for range 700 {
|
||
|
|
i := int(g.Next() % uint64(n))
|
||
|
|
j := int(g.Next() % uint64(n))
|
||
|
|
if i == j {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
entries = append(entries, entry{i, j})
|
||
|
|
deg[i]++
|
||
|
|
deg[j]++
|
||
|
|
}
|
||
|
|
idx := make([]int64, 0, 2*len(entries)+2*n)
|
||
|
|
vals := make([]float64, 0, 2*len(entries)+n)
|
||
|
|
add := func(r, c int, v float64) {
|
||
|
|
idx = append(idx, int64(r), int64(c))
|
||
|
|
vals = append(vals, v)
|
||
|
|
}
|
||
|
|
for _, e := range entries {
|
||
|
|
add(e.r, e.c, -1)
|
||
|
|
add(e.c, e.r, -1)
|
||
|
|
}
|
||
|
|
for i := range n {
|
||
|
|
add(i, i, float64(deg[i])+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{n, n})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSparseCOO: %v", err)
|
||
|
|
}
|
||
|
|
return coo
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestSparseCholeskyMinimumDegreeOrdering pins the minimum degree
|
||
|
|
// order's property: on both a shuffled mesh and a long-range
|
||
|
|
// irregular pattern its factor stores fewer non-zeros than the
|
||
|
|
// reverse Cuthill-McKee factor, and the solve stays exact. The
|
||
|
|
// measured numbers sit far inside the bounds and land in the log.
|
||
|
|
func TestSparseCholeskyMinimumDegreeOrdering(t *testing.T) {
|
||
|
|
coo := gridLaplacianCOOShuffled(t, 15, 15, true)
|
||
|
|
nat, err := NewSparseCholesky(coo, SparseOrderingNatural)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSparseCholesky(natural): %v", err)
|
||
|
|
}
|
||
|
|
rcm, err := NewSparseCholesky(coo, SparseOrderingReverseCuthillMcKee)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSparseCholesky(rcm): %v", err)
|
||
|
|
}
|
||
|
|
md, err := NewSparseCholesky(coo, SparseOrderingMinimumDegree)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSparseCholesky(md): %v", err)
|
||
|
|
}
|
||
|
|
t.Logf("grid n=225: natural %d, rcm %d, md %d", nat.NNZ(), rcm.NNZ(), md.NNZ())
|
||
|
|
if md.NNZ() >= rcm.NNZ() {
|
||
|
|
t.Fatalf("md fill %d is not below rcm fill %d on the grid", md.NNZ(), rcm.NNZ())
|
||
|
|
}
|
||
|
|
// Long-range irregular pattern: both orders must still solve, and
|
||
|
|
// the minimum degree order must keep its edge.
|
||
|
|
irr := irregularSPDCOO(t, 11, 300)
|
||
|
|
csr, err := CSRFromCOO(irr)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("CSRFromCOO: %v", err)
|
||
|
|
}
|
||
|
|
xTrue := core.New(core.Float, 300)
|
||
|
|
for i := range 300 {
|
||
|
|
xTrue.RawFloats()[i] = math.Cos(0.3*float64(i)) + float64(i%7)
|
||
|
|
}
|
||
|
|
b, err := csr.MatVec(xTrue)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("MatVec: %v", err)
|
||
|
|
}
|
||
|
|
rcmI, err := NewSparseCholesky(irr, SparseOrderingReverseCuthillMcKee)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSparseCholesky(rcm): %v", err)
|
||
|
|
}
|
||
|
|
mdI, err := NewSparseCholesky(irr, SparseOrderingMinimumDegree)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSparseCholesky(md): %v", err)
|
||
|
|
}
|
||
|
|
t.Logf("irregular n=300: rcm %d, md %d", rcmI.NNZ(), mdI.NNZ())
|
||
|
|
if mdI.NNZ() >= rcmI.NNZ() {
|
||
|
|
t.Fatalf("md fill %d is not below rcm fill %d on the irregular pattern", mdI.NNZ(), rcmI.NNZ())
|
||
|
|
}
|
||
|
|
for name, f := range map[string]*SparseCholesky{"rcm": rcmI, "md": mdI} {
|
||
|
|
x, err := f.Solve(b)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("%s solve: %v", name, err)
|
||
|
|
}
|
||
|
|
for i := range 300 {
|
||
|
|
if math.Abs(x.FloatAt(i)-xTrue.FloatAt(i)) > 1e-9 {
|
||
|
|
t.Fatalf("%s: entry %d error %.3g", name, i, math.Abs(x.FloatAt(i)-xTrue.FloatAt(i)))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
perm := md.Permutation()
|
||
|
|
sorted := slices.Clone(perm)
|
||
|
|
slices.Sort(sorted)
|
||
|
|
for i := range sorted {
|
||
|
|
if sorted[i] != i {
|
||
|
|
t.Fatalf("permutation entry %d holds %d; not a permutation", i, sorted[i])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestSparseCholeskyMatchesDense factors the same grid Laplacian as a
|
||
|
|
// dense matrix and requires the two solvers to agree: the sparse
|
||
|
|
// factorisation is a different algorithm for the same A⁻¹.
|
||
|
|
func TestSparseCholeskyMatchesDense(t *testing.T) {
|
||
|
|
const w, h = 8, 8
|
||
|
|
coo := gridLaplacianCOO(t, w, h)
|
||
|
|
n := w * h
|
||
|
|
csr, err := CSRFromCOO(coo)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("CSRFromCOO: %v", err)
|
||
|
|
}
|
||
|
|
xTrue := core.New(core.Float, n)
|
||
|
|
for i := range n {
|
||
|
|
xTrue.RawFloats()[i] = math.Cos(0.3*float64(i)) + float64(i%5)
|
||
|
|
}
|
||
|
|
b, err := csr.MatVec(xTrue)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("MatVec: %v", err)
|
||
|
|
}
|
||
|
|
f, err := NewSparseCholesky(coo, SparseOrderingReverseCuthillMcKee)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSparseCholesky: %v", err)
|
||
|
|
}
|
||
|
|
sparse, err := f.Solve(b)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Solve: %v", err)
|
||
|
|
}
|
||
|
|
denseVals := make([]float64, n*n)
|
||
|
|
nnz := coo.Indices.Shape()[0]
|
||
|
|
for i := range nnz {
|
||
|
|
r := int(coo.Indices.RawInts()[i*2])
|
||
|
|
c := int(coo.Indices.RawInts()[i*2+1])
|
||
|
|
denseVals[r*n+c] = coo.Values.FloatAt(i)
|
||
|
|
}
|
||
|
|
dense, err := Solve(floatsToArray(denseVals, []int{n, n}), b)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("dense Solve: %v", err)
|
||
|
|
}
|
||
|
|
scale := 0.0
|
||
|
|
for i := range n {
|
||
|
|
if d := math.Abs(dense.FloatAt(i)); d > scale {
|
||
|
|
scale = d
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for i := range n {
|
||
|
|
if math.Abs(sparse.FloatAt(i)-dense.FloatAt(i)) > 1e-9*scale {
|
||
|
|
t.Fatalf("entry %d: sparse %.12g vs dense %.12g", i, sparse.FloatAt(i), dense.FloatAt(i))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestSparseCholeskyFillMeasures pins the property the orderings
|
||
|
|
// exist for: on a shuffled mesh pattern the reverse Cuthill-McKee
|
||
|
|
// factor must store markedly fewer non-zeros than the natural order
|
||
|
|
// factor. The bounds carry slack on purpose; the measured numbers
|
||
|
|
// land far inside them and the log line records them, so a regression
|
||
|
|
// in the ordering shows up as a test failure, not as a slow solver.
|
||
|
|
func TestSparseCholeskyFillMeasures(t *testing.T) {
|
||
|
|
coo := gridLaplacianCOOShuffled(t, 15, 15, true)
|
||
|
|
natural, err := NewSparseCholesky(coo, SparseOrderingNatural)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSparseCholesky(natural): %v", err)
|
||
|
|
}
|
||
|
|
rcm, err := NewSparseCholesky(coo, SparseOrderingReverseCuthillMcKee)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSparseCholesky(rcm): %v", err)
|
||
|
|
}
|
||
|
|
t.Logf("n=225 shuffled: natural L nnz %d, rcm L nnz %d", natural.NNZ(), rcm.NNZ())
|
||
|
|
if rcm.NNZ() >= natural.NNZ() {
|
||
|
|
t.Fatalf("rcm fill %d is not below natural fill %d", rcm.NNZ(), natural.NNZ())
|
||
|
|
}
|
||
|
|
if rcm.NNZ() > natural.NNZ()/2 {
|
||
|
|
t.Fatalf("rcm fill %d did not at least halve natural fill %d", rcm.NNZ(), natural.NNZ())
|
||
|
|
}
|
||
|
|
// Both factors must still solve: the ordering changes the fill,
|
||
|
|
// never the answer.
|
||
|
|
csr, err := CSRFromCOO(coo)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("CSRFromCOO: %v", err)
|
||
|
|
}
|
||
|
|
xTrue := core.New(core.Float, 225)
|
||
|
|
for i := range xTrue.Len() {
|
||
|
|
xTrue.RawFloats()[i] = math.Cos(0.7*float64(i)) + float64(i%11)*0.2
|
||
|
|
}
|
||
|
|
b, err := csr.MatVec(xTrue)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("MatVec: %v", err)
|
||
|
|
}
|
||
|
|
for name, f := range map[string]*SparseCholesky{"natural": natural, "rcm": rcm} {
|
||
|
|
x, err := f.Solve(b)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("%s solve: %v", name, err)
|
||
|
|
}
|
||
|
|
for i := range xTrue.Len() {
|
||
|
|
if math.Abs(x.FloatAt(i)-xTrue.FloatAt(i)) > 1e-9 {
|
||
|
|
t.Fatalf("%s: entry %d error %.3g", name, i, math.Abs(x.FloatAt(i)-xTrue.FloatAt(i)))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
perm := rcm.Permutation()
|
||
|
|
sorted := slices.Clone(perm)
|
||
|
|
slices.Sort(sorted)
|
||
|
|
for i := range sorted {
|
||
|
|
if sorted[i] != i {
|
||
|
|
t.Fatalf("permutation entry %d holds %d; not a permutation", i, sorted[i])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// The permutation is a copy: moving the caller's slice must not
|
||
|
|
// move the factor's.
|
||
|
|
perm[0] = -1
|
||
|
|
if rcm.Permutation()[0] == -1 {
|
||
|
|
t.Fatal("Permutation exposed the factor's internal slice")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSparseCholeskyRefusals(t *testing.T) {
|
||
|
|
good := gridLaplacianCOO(t, 4, 4)
|
||
|
|
if _, err := NewSparseCholesky(good, SparseOrdering(7)); err == nil {
|
||
|
|
t.Fatal("an unknown ordering was accepted")
|
||
|
|
}
|
||
|
|
// A stored upper entry without its lower counterpart is a silent
|
||
|
|
// asymmetry the factor refuses to inherit. Entries: (0,0)=4,
|
||
|
|
// (0,1)=1, (1,1)=4, (1,2)=1, (2,2)=4, (2,3)=1, (3,3)=4: the upper
|
||
|
|
// (0,1) has no lower (1,0).
|
||
|
|
oneWay, err := core.NewSparseCOO(
|
||
|
|
mustInts(t, []int64{0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3}, 7, 2),
|
||
|
|
floatsToArray([]float64{4, 1, 4, 1, 4, 1, 4}, []int{7}),
|
||
|
|
[]int{4, 4})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSparseCOO: %v", err)
|
||
|
|
}
|
||
|
|
if _, err := NewSparseCholesky(oneWay, SparseOrderingNatural); err == nil || !strings.Contains(err.Error(), "counterpart") {
|
||
|
|
t.Fatalf("one-sided upper entry: %v", err)
|
||
|
|
}
|
||
|
|
// A conflicting pair refuses too. Entries: (0,0)=4, (0,1)=1,
|
||
|
|
// (1,1)=4, (1,2)=2, (2,2)=4, (1,3)=3, (3,3)=4, (2,1)=5: the upper
|
||
|
|
// (1,2)=2 and the lower (2,1)=5 disagree.
|
||
|
|
conflicting, err := core.NewSparseCOO(
|
||
|
|
mustInts(t, []int64{0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 1, 3, 3, 3, 2, 1}, 8, 2),
|
||
|
|
floatsToArray([]float64{4, 1, 4, 2, 4, 3, 4, 5}, []int{8}),
|
||
|
|
[]int{4, 4})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSparseCOO: %v", err)
|
||
|
|
}
|
||
|
|
if _, err := NewSparseCholesky(conflicting, SparseOrderingNatural); err == nil || !strings.Contains(err.Error(), "counterpart") {
|
||
|
|
t.Fatalf("conflicting upper entry: %v", err)
|
||
|
|
}
|
||
|
|
// Not positive definite: the zero diagonal has no square root.
|
||
|
|
sing := triDiagCOO(t, 4, 1, 0, 1)
|
||
|
|
if _, err := NewSparseCholesky(sing, SparseOrderingNatural); err == nil || !strings.Contains(err.Error(), "positive definite") {
|
||
|
|
t.Fatalf("zero diagonal: %v", err)
|
||
|
|
}
|
||
|
|
// Non-finite stored value.
|
||
|
|
bad, err := core.NewSparseCOO(
|
||
|
|
mustInts(t, []int64{0, 0, 1, 1}, 2, 2),
|
||
|
|
floatsToArray([]float64{4, math.NaN()}, []int{2}),
|
||
|
|
[]int{2, 2})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSparseCOO: %v", err)
|
||
|
|
}
|
||
|
|
if _, err := NewSparseCholesky(bad, SparseOrderingNatural); err == nil || !strings.Contains(err.Error(), "finite") {
|
||
|
|
t.Fatalf("NaN entry: %v", err)
|
||
|
|
}
|
||
|
|
// Rectangular input.
|
||
|
|
rect, err := core.NewSparseCOO(
|
||
|
|
mustInts(t, []int64{0, 0}, 1, 2),
|
||
|
|
floatsToArray([]float64{1}, []int{1}),
|
||
|
|
[]int{1, 2})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSparseCOO: %v", err)
|
||
|
|
}
|
||
|
|
if _, err := NewSparseCholesky(rect, SparseOrderingNatural); err == nil {
|
||
|
|
t.Fatal("a rectangular matrix was accepted")
|
||
|
|
}
|
||
|
|
// Solve-side refusals.
|
||
|
|
f, err := NewSparseCholesky(good, SparseOrderingNatural)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSparseCholesky: %v", err)
|
||
|
|
}
|
||
|
|
if _, err := f.Solve(core.New(core.Float, 3, 3)); err == nil {
|
||
|
|
t.Fatal("a rank 2 right hand side was accepted")
|
||
|
|
}
|
||
|
|
if _, err := f.Solve(floatsToArray([]float64{1, 2, 3}, []int{3})); err == nil {
|
||
|
|
t.Fatal("a short right hand side was accepted")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestSparseCholeskyIsDeterministic factors the same matrix twice and
|
||
|
|
// requires the factor's stored values to come out bit for bit equal,
|
||
|
|
// the contract every Tensor entry point carries.
|
||
|
|
func TestSparseCholeskyIsDeterministic(t *testing.T) {
|
||
|
|
coo := gridLaplacianCOO(t, 9, 9)
|
||
|
|
f1, err := NewSparseCholesky(coo, SparseOrderingReverseCuthillMcKee)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("first factorisation: %v", err)
|
||
|
|
}
|
||
|
|
f2, err := NewSparseCholesky(coo, SparseOrderingReverseCuthillMcKee)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("second factorisation: %v", err)
|
||
|
|
}
|
||
|
|
if f1.NNZ() != f2.NNZ() {
|
||
|
|
t.Fatalf("factor sizes differ: %d vs %d", f1.NNZ(), f2.NNZ())
|
||
|
|
}
|
||
|
|
for i := range f1.values {
|
||
|
|
if f1.values[i] != f2.values[i] {
|
||
|
|
t.Fatalf("value %d differs: %.17g vs %.17g", i, f1.values[i], f2.values[i])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if slices.Compare(f1.perm, f2.perm) != 0 {
|
||
|
|
t.Fatal("permutations differ")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestCSCCanonicalisation checks CSCFromCOO's contract beside
|
||
|
|
// CSRFromCOO's: duplicates sum, explicit zeros drop, every column's
|
||
|
|
// row indices are sorted and unique, and the transpose round trips
|
||
|
|
// agree entry for entry with the direct conversions.
|
||
|
|
func TestCSCCanonicalisation(t *testing.T) {
|
||
|
|
// Entries: (2,0)=3, (2,0)=1 duplicate, (1,1)=5, (0,2)=7 explicit
|
||
|
|
// zero, (0,2)=2.
|
||
|
|
coo, err := core.NewSparseCOO(
|
||
|
|
mustInts(t, []int64{2, 0, 2, 0, 1, 1, 0, 2, 0, 2}, 5, 2),
|
||
|
|
floatsToArray([]float64{3, 1, 5, 0, 2}, []int{5}),
|
||
|
|
[]int{3, 3})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSparseCOO: %v", err)
|
||
|
|
}
|
||
|
|
csc, err := CSCFromCOO(coo)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("CSCFromCOO: %v", err)
|
||
|
|
}
|
||
|
|
if csc.NNZ() != 3 {
|
||
|
|
t.Fatalf("nnz %d after merging and zero drop, want 3", csc.NNZ())
|
||
|
|
}
|
||
|
|
if csc.Values[0] != 4 {
|
||
|
|
t.Fatalf("duplicates did not sum: column 0 first value %g", csc.Values[0])
|
||
|
|
}
|
||
|
|
for j := range csc.Cols {
|
||
|
|
rows := csc.RowIdx[csc.ColStart[j]:csc.ColStart[j+1]]
|
||
|
|
if !slices.IsSorted(rows) {
|
||
|
|
t.Fatalf("column %d rows not sorted: %v", j, rows)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
csr, err := CSRFromCOO(coo)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("CSRFromCOO: %v", err)
|
||
|
|
}
|
||
|
|
round, err := csc.ToCSR()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("ToCSR: %v", err)
|
||
|
|
}
|
||
|
|
if slices.Compare(csr.RowStart, round.RowStart) != 0 ||
|
||
|
|
slices.Compare(csr.ColIdx, round.ColIdx) != 0 ||
|
||
|
|
slices.Compare(csr.Values, round.Values) != 0 {
|
||
|
|
t.Fatal("CSC to CSR round trip disagrees with the direct conversion")
|
||
|
|
}
|
||
|
|
back, err := csr.ToCSC()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("ToCSC: %v", err)
|
||
|
|
}
|
||
|
|
if slices.Compare(csc.ColStart, back.ColStart) != 0 ||
|
||
|
|
slices.Compare(csc.RowIdx, back.RowIdx) != 0 ||
|
||
|
|
slices.Compare(csc.Values, back.Values) != 0 {
|
||
|
|
t.Fatal("CSR to CSC round trip disagrees with the direct conversion")
|
||
|
|
}
|
||
|
|
// MatVec over the CSC must answer what the CSR answers.
|
||
|
|
x := floatsToArray([]float64{1, -2, 3}, []int{3})
|
||
|
|
ycsr, err := csr.MatVec(x)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("CSR MatVec: %v", err)
|
||
|
|
}
|
||
|
|
ycsc, err := csc.MatVec(x)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("CSC MatVec: %v", err)
|
||
|
|
}
|
||
|
|
for i := range ycsr.Len() {
|
||
|
|
if ycsr.FloatAt(i) != ycsc.FloatAt(i) {
|
||
|
|
t.Fatalf("MatVec row %d: csr %.17g vs csc %.17g", i, ycsr.FloatAt(i), ycsc.FloatAt(i))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if _, err := csc.MatVec(floatsToArray([]float64{1, 2}, []int{2})); err == nil {
|
||
|
|
t.Fatal("a wrong-length vector was accepted")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func mustInts(t *testing.T, vals []int64, shape ...int) *core.Array {
|
||
|
|
t.Helper()
|
||
|
|
a, err := core.FromInts(vals, shape...)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("FromInts: %v", err)
|
||
|
|
}
|
||
|
|
return a
|
||
|
|
}
|