// Copyright ©2015 The Gonum Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package mat import ( "fmt" "os" "reflect" "testing" "golang.org/x/exp/rand" "gonum.org/v1/gonum/blas" "gonum.org/v1/gonum/blas/blas64" "gonum.org/v1/gonum/floats/scalar" ) func TestNewSymmetric(t *testing.T) { t.Parallel() for i, test := range []struct { data []float64 n int mat *SymDense }{ { data: []float64{ 1, 2, 3, 4, 5, 6, 7, 8, 9, }, n: 3, mat: &SymDense{ mat: blas64.Symmetric{ N: 3, Stride: 3, Uplo: blas.Upper, Data: []float64{1, 2, 3, 4, 5, 6, 7, 8, 9}, }, cap: 3, }, }, } { sym := NewSymDense(test.n, test.data) rows, cols := sym.Dims() if rows != test.n { t.Errorf("unexpected number of rows for test %d: got: %d want: %d", i, rows, test.n) } if cols != test.n { t.Errorf("unexpected number of cols for test %d: got: %d want: %d", i, cols, test.n) } if !reflect.DeepEqual(sym, test.mat) { t.Errorf("unexpected data slice for test %d: got: %v want: %v", i, sym, test.mat) } m := NewDense(test.n, test.n, test.data) if !reflect.DeepEqual(sym.mat.Data, m.mat.Data) { t.Errorf("unexpected data slice mismatch for test %d: got: %v want: %v", i, sym.mat.Data, m.mat.Data) } } panicked, message := panics(func() { NewSymDense(3, []float64{1, 2}) }) if !panicked || message != ErrShape.Error() { t.Error("expected panic for invalid data slice length") } } func TestSymAtSet(t *testing.T) { t.Parallel() sym := &SymDense{ mat: blas64.Symmetric{ N: 3, Stride: 3, Uplo: blas.Upper, Data: []float64{1, 2, 3, 4, 5, 6, 7, 8, 9}, }, cap: 3, } rows, cols := sym.Dims() // Check At out of bounds for _, row := range []int{-1, rows, rows + 1} { panicked, message := panics(func() { sym.At(row, 0) }) if !panicked || message != ErrRowAccess.Error() { t.Errorf("expected panic for invalid row access N=%d r=%d", rows, row) } } for _, col := range []int{-1, cols, cols + 1} { panicked, message := panics(func() { sym.At(0, col) }) if !panicked || message != ErrColAccess.Error() { t.Errorf("expected panic for invalid column access N=%d c=%d", cols, col) } } // Check Set out of bounds for _, row := range []int{-1, rows, rows + 1} { panicked, message := panics(func() { sym.SetSym(row, 0, 1.2) }) if !panicked || message != ErrRowAccess.Error() { t.Errorf("expected panic for invalid row access N=%d r=%d", rows, row) } } for _, col := range []int{-1, cols, cols + 1} { panicked, message := panics(func() { sym.SetSym(0, col, 1.2) }) if !panicked || message != ErrColAccess.Error() { t.Errorf("expected panic for invalid column access N=%d c=%d", cols, col) } } for _, st := range []struct { row, col int orig, new float64 }{ {row: 1, col: 2, orig: 6, new: 15}, {row: 2, col: 1, orig: 15, new: 12}, } { if e := sym.At(st.row, st.col); e != st.orig { t.Errorf("unexpected value for At(%d, %d): got: %v want: %v", st.row, st.col, e, st.orig) } if e := sym.At(st.col, st.row); e != st.orig { t.Errorf("unexpected value for At(%d, %d): got: %v want: %v", st.col, st.row, e, st.orig) } sym.SetSym(st.row, st.col, st.new) if e := sym.At(st.row, st.col); e != st.new { t.Errorf("unexpected value for At(%d, %d) after SetSym(%[1]d, %[2]d, %[4]v): got: %[3]v want: %v", st.row, st.col, e, st.new) } if e := sym.At(st.col, st.row); e != st.new { t.Errorf("unexpected value for At(%d, %d) after SetSym(%[2]d, %[1]d, %[4]v): got: %[3]v want: %v", st.col, st.row, e, st.new) } } } func TestSymDenseZero(t *testing.T) { t.Parallel() // Elements that equal 1 should be set to zero, elements that equal -1 // should remain unchanged. for _, test := range []*SymDense{ { mat: blas64.Symmetric{ Uplo: blas.Upper, N: 4, Stride: 5, Data: []float64{ 1, 1, 1, 1, -1, -1, 1, 1, 1, -1, -1, -1, 1, 1, -1, -1, -1, -1, 1, -1, }, }, }, } { dataCopy := make([]float64, len(test.mat.Data)) copy(dataCopy, test.mat.Data) test.Zero() for i, v := range test.mat.Data { if dataCopy[i] != -1 && v != 0 { t.Errorf("Matrix not zeroed in bounds") } if dataCopy[i] == -1 && v != -1 { t.Errorf("Matrix zeroed out of bounds") } } } } func TestSymDiagView(t *testing.T) { t.Parallel() for cas, test := range []*SymDense{ NewSymDense(1, []float64{1}), NewSymDense(2, []float64{1, 2, 2, 3}), NewSymDense(3, []float64{1, 2, 3, 2, 4, 5, 3, 5, 6}), } { testDiagView(t, cas, test) } } func TestSymAdd(t *testing.T) { t.Parallel() rnd := rand.New(rand.NewSource(1)) for _, test := range []struct { n int }{ {n: 1}, {n: 2}, {n: 3}, {n: 4}, {n: 5}, {n: 10}, } { n := test.n a := NewSymDense(n, nil) for i := range a.mat.Data { a.mat.Data[i] = rnd.Float64() } b := NewSymDense(n, nil) for i := range a.mat.Data { b.mat.Data[i] = rnd.Float64() } var m Dense m.Add(a, b) // Check with new receiver var s SymDense s.AddSym(a, b) for i := 0; i < n; i++ { for j := i; j < n; j++ { want := m.At(i, j) if got := s.At(i, j); got != want { t.Errorf("unexpected value for At(%d, %d): got: %v want: %v", i, j, got, want) } } } // Check with equal receiver s.CopySym(a) s.AddSym(&s, b) for i := 0; i < n; i++ { for j := i; j < n; j++ { want := m.At(i, j) if got := s.At(i, j); got != want { t.Errorf("unexpected value for At(%d, %d): got: %v want: %v", i, j, got, want) } } } } method := func(receiver, a, b Matrix) { type addSymer interface { AddSym(a, b Symmetric) } rd := receiver.(addSymer) rd.AddSym(a.(Symmetric), b.(Symmetric)) } denseComparison := func(receiver, a, b *Dense) { receiver.Add(a, b) } testTwoInput(t, "AddSym", &SymDense{}, method, denseComparison, legalTypesSym, legalSizeSameSquare, 1e-14) } func TestCopy(t *testing.T) { t.Parallel() rnd := rand.New(rand.NewSource(1)) for _, test := range []struct { n int }{ {n: 1}, {n: 2}, {n: 3}, {n: 4}, {n: 5}, {n: 10}, } { n := test.n a := NewSymDense(n, nil) for i := range a.mat.Data { a.mat.Data[i] = rnd.Float64() } s := NewSymDense(n, nil) s.CopySym(a) for i := 0; i < n; i++ { for j := i; j < n; j++ { want := a.At(i, j) if got := s.At(i, j); got != want { t.Errorf("unexpected value for At(%d, %d): got: %v want: %v", i, j, got, want) } } } } } // TODO(kortschak) Roll this into testOneInput when it exists. // https://github.com/gonum/matrix/issues/171 func TestSymCopyPanic(t *testing.T) { t.Parallel() var ( a SymDense n int ) m := NewSymDense(1, nil) panicked, message := panics(func() { n = m.CopySym(&a) }) if panicked { t.Errorf("unexpected panic: %v", message) } if n != 0 { t.Errorf("unexpected n: got: %d want: 0", n) } } func TestSymRankOne(t *testing.T) { t.Parallel() rnd := rand.New(rand.NewSource(1)) const tol = 1e-15 for _, test := range []struct { n int }{ {n: 1}, {n: 2}, {n: 3}, {n: 4}, {n: 5}, {n: 10}, } { n := test.n alpha := 2.0 a := NewSymDense(n, nil) for i := range a.mat.Data { a.mat.Data[i] = rnd.Float64() } x := make([]float64, n) for i := range x { x[i] = rnd.Float64() } xMat := NewDense(n, 1, x) var m Dense m.Mul(xMat, xMat.T()) m.Scale(alpha, &m) m.Add(&m, a) // Check with new receiver s := NewSymDense(n, nil) s.SymRankOne(a, alpha, NewVecDense(len(x), x)) for i := 0; i < n; i++ { for j := i; j < n; j++ { want := m.At(i, j) if got := s.At(i, j); !scalar.EqualWithinAbsOrRel(got, want, tol, tol) { t.Errorf("unexpected value for At(%d, %d): got: %v want: %v", i, j, got, want) } } } // Check with reused receiver copy(s.mat.Data, a.mat.Data) s.SymRankOne(s, alpha, NewVecDense(len(x), x)) for i := 0; i < n; i++ { for j := i; j < n; j++ { want := m.At(i, j) if got := s.At(i, j); !scalar.EqualWithinAbsOrRel(got, want, tol, tol) { t.Errorf("unexpected value for At(%d, %d): got: %v want: %v", i, j, got, want) } } } } alpha := 3.0 method := func(receiver, a, b Matrix) { type SymRankOner interface { SymRankOne(a Symmetric, alpha float64, x Vector) } rd := receiver.(SymRankOner) rd.SymRankOne(a.(Symmetric), alpha, b.(Vector)) } denseComparison := func(receiver, a, b *Dense) { var tmp Dense tmp.Mul(b, b.T()) tmp.Scale(alpha, &tmp) receiver.Add(a, &tmp) } legalTypes := func(a, b Matrix) bool { _, ok := a.(Symmetric) if !ok { return false } _, ok = b.(Vector) return ok } legalSize := func(ar, ac, br, bc int) bool { if ar != ac { return false } return br == ar } testTwoInput(t, "SymRankOne", &SymDense{}, method, denseComparison, legalTypes, legalSize, 1e-14) } func TestIssue250SymRankOne(t *testing.T) { t.Parallel() x := NewVecDense(5, []float64{1, 2, 3, 4, 5}) var s1, s2 SymDense s1.SymRankOne(NewSymDense(5, nil), 1, x) s2.SymRankOne(NewSymDense(5, nil), 1, x) s2.SymRankOne(NewSymDense(5, nil), 1, x) if !Equal(&s1, &s2) { t.Error("unexpected result from repeat") } } func TestRankTwo(t *testing.T) { t.Parallel() rnd := rand.New(rand.NewSource(1)) for _, test := range []struct { n int }{ {n: 1}, {n: 2}, {n: 3}, {n: 4}, {n: 5}, {n: 10}, } { n := test.n alpha := 2.0 a := NewSymDense(n, nil) for i := range a.mat.Data { a.mat.Data[i] = rnd.Float64() } x := make([]float64, n) y := make([]float64, n) for i := range x { x[i] = rnd.Float64() y[i] = rnd.Float64() } xMat := NewDense(n, 1, x) yMat := NewDense(n, 1, y) var m Dense m.Mul(xMat, yMat.T()) var tmp Dense tmp.Mul(yMat, xMat.T()) m.Add(&m, &tmp) m.Scale(alpha, &m) m.Add(&m, a) // Check with new receiver s := NewSymDense(n, nil) s.RankTwo(a, alpha, NewVecDense(len(x), x), NewVecDense(len(y), y)) for i := 0; i < n; i++ { for j := i; j < n; j++ { if !scalar.EqualWithinAbsOrRel(s.At(i, j), m.At(i, j), 1e-14, 1e-14) { t.Errorf("unexpected element value at (%d,%d): got: %f want: %f", i, j, m.At(i, j), s.At(i, j)) } } } // Check with reused receiver copy(s.mat.Data, a.mat.Data) s.RankTwo(s, alpha, NewVecDense(len(x), x), NewVecDense(len(y), y)) for i := 0; i < n; i++ { for j := i; j < n; j++ { if !scalar.EqualWithinAbsOrRel(s.At(i, j), m.At(i, j), 1e-14, 1e-14) { t.Errorf("unexpected element value at (%d,%d): got: %f want: %f", i, j, m.At(i, j), s.At(i, j)) } } } } } func TestSymRankK(t *testing.T) { t.Parallel() alpha := 3.0 method := func(receiver, a, b Matrix) { type SymRankKer interface { SymRankK(a Symmetric, alpha float64, x Matrix) } rd := receiver.(SymRankKer) rd.SymRankK(a.(Symmetric), alpha, b) } denseComparison := func(receiver, a, b *Dense) { var tmp Dense tmp.Mul(b, b.T()) tmp.Scale(alpha, &tmp) receiver.Add(a, &tmp) } legalTypes := func(a, b Matrix) bool { _, ok := a.(Symmetric) return ok } legalSize := func(ar, ac, br, bc int) bool { if ar != ac { return false } return br == ar } testTwoInput(t, "SymRankK", &SymDense{}, method, denseComparison, legalTypes, legalSize, 1e-14) } func TestSymOuterK(t *testing.T) { t.Parallel() for _, f := range []float64{0.5, 1, 3} { method := func(receiver, x Matrix) { type SymOuterKer interface { SymOuterK(alpha float64, x Matrix) } rd := receiver.(SymOuterKer) rd.SymOuterK(f, x) } denseComparison := func(receiver, x *Dense) { receiver.Mul(x, x.T()) receiver.Scale(f, receiver) } testOneInput(t, "SymOuterK", &SymDense{}, method, denseComparison, isAnyType, isAnySize, 1e-14) } } func TestIssue250SymOuterK(t *testing.T) { t.Parallel() x := NewVecDense(5, []float64{1, 2, 3, 4, 5}) var s1, s2 SymDense s1.SymOuterK(1, x) s2.SymOuterK(1, x) s2.SymOuterK(1, x) if !Equal(&s1, &s2) { t.Error("unexpected result from repeat") } } func TestScaleSym(t *testing.T) { t.Parallel() for _, f := range []float64{0.5, 1, 3} { method := func(receiver, a Matrix) { type ScaleSymer interface { ScaleSym(f float64, a Symmetric) } rd := receiver.(ScaleSymer) rd.ScaleSym(f, a.(Symmetric)) } denseComparison := func(receiver, a *Dense) { receiver.Scale(f, a) } testOneInput(t, "ScaleSym", &SymDense{}, method, denseComparison, legalTypeSym, isSquare, 1e-14) } } func TestSubsetSym(t *testing.T) { t.Parallel() for _, test := range []struct { a *SymDense dims []int ans *SymDense }{ { a: NewSymDense(3, []float64{ 1, 2, 3, 0, 4, 5, 0, 0, 6, }), dims: []int{0, 2}, ans: NewSymDense(2, []float64{ 1, 3, 0, 6, }), }, { a: NewSymDense(3, []float64{ 1, 2, 3, 0, 4, 5, 0, 0, 6, }), dims: []int{2, 0}, ans: NewSymDense(2, []float64{ 6, 3, 0, 1, }), }, { a: NewSymDense(3, []float64{ 1, 2, 3, 0, 4, 5, 0, 0, 6, }), dims: []int{1, 1, 1}, ans: NewSymDense(3, []float64{ 4, 4, 4, 0, 4, 4, 0, 0, 4, }), }, } { var s SymDense s.SubsetSym(test.a, test.dims) if !Equal(&s, test.ans) { t.Errorf("SubsetSym mismatch dims %v\nGot:\n% v\nWant:\n% v\n", test.dims, s, test.ans) } } dims := []int{0, 2} maxDim := dims[0] for _, v := range dims { if maxDim < v { maxDim = v } } method := func(receiver, a Matrix) { type SubsetSymer interface { SubsetSym(a Symmetric, set []int) } rd := receiver.(SubsetSymer) rd.SubsetSym(a.(Symmetric), dims) } denseComparison := func(receiver, a *Dense) { *receiver = *NewDense(len(dims), len(dims), nil) sz := len(dims) for i := 0; i < sz; i++ { for j := 0; j < sz; j++ { receiver.Set(i, j, a.At(dims[i], dims[j])) } } } legalSize := func(ar, ac int) bool { return ar == ac && ar > maxDim } testOneInput(t, "SubsetSym", &SymDense{}, method, denseComparison, legalTypeSym, legalSize, 0) } func TestViewGrowSquare(t *testing.T) { t.Parallel() // n is the size of the original SymDense. // The first view uses start1, span1. The second view uses start2, span2 on // the first view. for _, test := range []struct { n, start1, span1, start2, span2 int }{ {10, 0, 10, 0, 10}, {10, 0, 8, 0, 8}, {10, 2, 8, 0, 6}, {10, 2, 7, 4, 2}, {10, 2, 6, 0, 5}, } { n := test.n s := NewSymDense(n, nil) for i := 0; i < n; i++ { for j := i; j < n; j++ { s.SetSym(i, j, float64((i+1)*n+j+1)) } } // Take a subset and check the view matches. start1 := test.start1 span1 := test.span1 v := s.sliceSym(start1, start1+span1) for i := 0; i < span1; i++ { for j := i; j < span1; j++ { if v.At(i, j) != s.At(start1+i, start1+j) { t.Errorf("View mismatch") } } } start2 := test.start2 span2 := test.span2 v2 := v.SliceSym(start2, start2+span2).(*SymDense) for i := 0; i < span2; i++ { for j := i; j < span2; j++ { if v2.At(i, j) != s.At(start1+start2+i, start1+start2+j) { t.Errorf("Second view mismatch") } } } // Check that a write to the view is reflected in the original. v2.SetSym(0, 0, 1.2) if s.At(start1+start2, start1+start2) != 1.2 { t.Errorf("Write to view not reflected in original") } // Grow the matrix back to the original view gn := n - start1 - start2 g := v2.GrowSym(gn - v2.SymmetricDim()).(*SymDense) g.SetSym(1, 1, 2.2) for i := 0; i < gn; i++ { for j := 0; j < gn; j++ { if g.At(i, j) != s.At(start1+start2+i, start1+start2+j) { t.Errorf("Grow mismatch") fmt.Printf("g=\n% v\n", Formatted(g)) fmt.Printf("s=\n% v\n", Formatted(s)) os.Exit(1) } } } // View g, then grow it and make sure all the elements were copied. gv := g.SliceSym(0, gn-1).(*SymDense) gg := gv.GrowSym(2) for i := 0; i < gn; i++ { for j := 0; j < gn; j++ { if g.At(i, j) != gg.At(i, j) { t.Errorf("Expand mismatch") } } } s.Reset() rg := s.GrowSym(n).(*SymDense) if rg.mat.Stride < n { t.Errorf("unexpected stride after GrowSym on empty matrix: got:%d want >= %d", rg.mat.Stride, n) } } } func TestPowPSD(t *testing.T) { t.Parallel() for cas, test := range []struct { a *SymDense pow float64 ans *SymDense }{ // Comparison with Matlab. { a: NewSymDense(2, []float64{10, 5, 5, 12}), pow: 0.5, ans: NewSymDense(2, []float64{3.065533767740645, 0.776210486171016, 0.776210486171016, 3.376017962209052}), }, { a: NewSymDense(2, []float64{11, -1, -1, 8}), pow: 0.5, ans: NewSymDense(2, []float64{3.312618742210524, -0.162963396980939, -0.162963396980939, 2.823728551267709}), }, { a: NewSymDense(2, []float64{10, 5, 5, 12}), pow: -0.5, ans: NewSymDense(2, []float64{0.346372134547712, -0.079637515547296, -0.079637515547296, 0.314517128328794}), }, { a: NewSymDense(3, []float64{15, -1, -3, -1, 8, 6, -3, 6, 14}), pow: 0.6, ans: NewSymDense(3, []float64{ 5.051214323034288, -0.163162161893975, -0.612153996497505, -0.163162161893976, 3.283474884617009, 1.432842761381493, -0.612153996497505, 1.432842761381494, 4.695873060862573, }), }, } { var s SymDense err := s.PowPSD(test.a, test.pow) if err != nil { panic("bad test") } if !EqualApprox(&s, test.ans, 1e-10) { t.Errorf("Case %d, pow mismatch", cas) fmt.Println(Formatted(&s)) fmt.Println(Formatted(test.ans)) } } // Compare with Dense.Pow rnd := rand.New(rand.NewSource(1)) for dim := 2; dim < 10; dim++ { for pow := 2; pow < 6; pow++ { a := NewDense(dim, dim, nil) for i := 0; i < dim; i++ { for j := 0; j < dim; j++ { a.Set(i, j, rnd.Float64()) } } var mat SymDense mat.SymOuterK(1, a) var sym SymDense err := sym.PowPSD(&mat, float64(pow)) if err != nil { t.Errorf("unexpected error: %v", err) } var dense Dense dense.Pow(&mat, pow) if !EqualApprox(&sym, &dense, 1e-10) { t.Errorf("Dim %d: pow mismatch", dim) } } } } func BenchmarkSymSum1000(b *testing.B) { symSumBench(b, 1000) } var symSumForBench float64 func symSumBench(b *testing.B, size int) { src := rand.NewSource(1) a := randSymDense(size, src) b.ResetTimer() for i := 0; i < b.N; i++ { symSumForBench = Sum(a) } } func randSymDense(size int, src rand.Source) *SymDense { rnd := rand.New(src) backData := make([]float64, size*size) for i := 0; i < size; i++ { backData[i*size+i] = rnd.Float64() for j := i + 1; j < size; j++ { v := rnd.Float64() backData[i*size+j] = v backData[j*size+i] = v } } s := NewSymDense(size, backData) return s }