// Copyright ©2021 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 r3 import ( "math" "testing" "golang.org/x/exp/rand" "gonum.org/v1/gonum/mat" "gonum.org/v1/gonum/num/quat" ) func TestMatAdd(t *testing.T) { const tol = 1e-16 rnd := rand.New(rand.NewSource(1)) for tc := 0; tc < 20; tc++ { a := randomMat(rnd) b := randomMat(rnd) var ( want mat.Dense got Mat ) want.Add(a, b) got.Add(a, b) if !mat.EqualApprox(&got, &want, tol) { t.Errorf("unexpected result for matrix add:\ngot:\n%v\nwant:\n%v", mat.Formatted(&got), mat.Formatted(&want)) } } } func TestMatSub(t *testing.T) { const tol = 1e-16 rnd := rand.New(rand.NewSource(1)) for tc := 0; tc < 20; tc++ { a := randomMat(rnd) b := randomMat(rnd) var ( want mat.Dense got Mat ) want.Sub(a, b) got.Sub(a, b) if !mat.EqualApprox(&got, &want, tol) { t.Errorf("unexpected result for matrix subtract:\ngot:\n%v\nwant:\n%v", mat.Formatted(&got), mat.Formatted(&want)) } } } func TestMatMul(t *testing.T) { const tol = 1e-14 rnd := rand.New(rand.NewSource(1)) for tc := 0; tc < 20; tc++ { a := randomMat(rnd) b := randomMat(rnd) var ( want mat.Dense got Mat ) want.Mul(a, b) got.Mul(a, b) if !mat.EqualApprox(&got, &want, tol) { t.Errorf("unexpected result for matrix multiply:\ngot:\n%v\nwant:\n%v", mat.Formatted(&got), mat.Formatted(&want)) } } } func TestMatScale(t *testing.T) { const tol = 1e-16 rnd := rand.New(rand.NewSource(1)) for tc := 0; tc < 20; tc++ { v := rnd.Float64() a := randomMat(rnd) var ( want mat.Dense got Mat ) want.Scale(v, a) got.Scale(v, a) if !mat.EqualApprox(&got, &want, tol) { t.Errorf("unexpected result for matrix scale:\ngot:\n%v\nwant:\n%v", mat.Formatted(&got), mat.Formatted(&want)) } } } func TestMatCloneFrom(t *testing.T) { const tol = 1e-16 rnd := rand.New(rand.NewSource(1)) for tc := 0; tc < 20; tc++ { want := randomMat(rnd) got := NewMat(nil) got.CloneFrom(want) if !mat.EqualApprox(got, want, tol) { t.Errorf("unexpected result from CloneFrom:\ngot:\n%v\nwant:\n%v", mat.Formatted(got), mat.Formatted(want)) } } } func TestSkew(t *testing.T) { const tol = 1e-16 rnd := rand.New(rand.NewSource(1)) for tc := 0; tc < 20; tc++ { sk := NewMat(nil) v1 := randomVec(rnd) v2 := randomVec(rnd) sk.Skew(v1) want := Cross(v1, v2) got := sk.MulVec(v2) if d := Sub(want, got); Dot(d, d) > tol { t.Errorf("r3.Cross(v1,v2) does not agree with r3.Skew(v1)*v2: got:%v want:%v", got, want) } } } func TestTranspose(t *testing.T) { const tol = 1e-16 rnd := rand.New(rand.NewSource(1)) for tc := 0; tc < 20; tc++ { d := mat.NewDense(3, 3, nil) m := randomMat(rnd) d.CloneFrom(m) mt := m.T() dt := d.T() if !mat.Equal(mt, dt) { t.Errorf("Dense.T() not equal to r3.Mat.T():\ngot:\n%v\nwant:\n%v", mat.Formatted(mt), mat.Formatted(dt)) } vd := mat.NewVecDense(3, nil) v := randomVec(rnd) vd.SetVec(0, v.X) vd.SetVec(1, v.Y) vd.SetVec(2, v.Z) vd.MulVec(dt, vd) want := Vec{X: vd.AtVec(0), Y: vd.AtVec(1), Z: vd.AtVec(2)} got := m.MulVecTrans(v) if d := Sub(want, got); Dot(d, d) > tol { t.Errorf("VecDense.MulVec(dense.T()) not agree with r3.Mat.MulVec(r3.Vec): got:%v want:%v", got, want) } } } func randomMat(rnd *rand.Rand) *Mat { m := Mat{new(array)} for iv := 0; iv < 9; iv++ { i := iv / 3 j := iv % 3 m.Set(i, j, (rnd.Float64()-0.5)*20) } return &m } func randomVec(rnd *rand.Rand) (v Vec) { v.X = (rnd.Float64() - 0.5) * 20 v.Y = (rnd.Float64() - 0.5) * 20 v.Z = (rnd.Float64() - 0.5) * 20 return v } func TestDet(t *testing.T) { const tol = 1e-11 rnd := rand.New(rand.NewSource(1)) for tc := 0; tc < 20; tc++ { m := randomMat(rnd) got := m.Det() want := mat.Det(m) if math.Abs(got-want) > tol { t.Errorf("r3.Mat.Det() not equal to mat.Det(). got %f, want %f", got, want) } } } func TestOuter(t *testing.T) { rnd := rand.New(rand.NewSource(1)) for tc := 0; tc < 20; tc++ { alpha := rnd.Float64() d := mat.NewDense(3, 3, nil) n := NewMat(nil) v1 := randomVec(rnd) v2 := randomVec(rnd) d1 := mat.NewVecDense(3, []float64{v1.X, v1.Y, v1.Z}) d2 := mat.NewVecDense(3, []float64{v2.X, v2.Y, v2.Z}) d.Outer(alpha, d1, d2) n.Outer(alpha, v1, v2) if !mat.Equal(d, n) { t.Error("matrices not equal") } } } func TestRotationMat(t *testing.T) { const tol = 1e-14 rnd := rand.New(rand.NewSource(1)) for tc := 0; tc < 20; tc++ { // Generate a random unit quaternion. q := quat.Number{Real: rnd.NormFloat64(), Imag: rnd.NormFloat64(), Jmag: rnd.NormFloat64(), Kmag: rnd.NormFloat64()} q = quat.Scale(1/quat.Abs(q), q) // Convert it to a rotation matrix R. r := Rotation(q).Mat() // Check that the matrix has the determinant approximately equal to 1. diff := math.Abs(r.Det() - 1) if diff > tol { t.Errorf("case %d: unexpected determinant of R; got=%f, want=1 (diff=%v)", tc, r.Det(), diff) continue } // Generate a random point. v := Vec{X: rnd.NormFloat64(), Y: rnd.NormFloat64(), Z: rnd.NormFloat64()} // Rotate it using the formula q*v*conj(q). want := Rotation(q).Rotate(v) // Rotate it using the rotation matrix R. got := r.MulVec(v) // Check that |got-want| is small. diff = Norm(Sub(got, want)) if diff > tol { t.Errorf("case %d: unexpected result; got=%f, want=%f, (diff=%v)", tc, got, want, diff) } } } func BenchmarkQuat(b *testing.B) { rnd := rand.New(rand.NewSource(1)) for i := 0; i < b.N; i++ { q := quat.Number{Real: rnd.Float64(), Imag: rnd.Float64(), Jmag: rnd.Float64(), Kmag: rnd.Float64()} if Rotation(q).Mat() == nil { b.Fatal("nil return") } } } var scalarFields = []struct { // This is the scalar field function. field func(p Vec) float64 gradient func(p Vec) Vec hessian func(p Vec) *Mat }{ { field: func(p Vec) float64 { // 4*x^3 + 5*y^2 + 3*z^4 z2 := p.Z * p.Z return 4*p.X*p.X*p.X + 5*p.Y*p.Y + 3*z2*z2 }, gradient: func(p Vec) Vec { return Vec{X: 12 * p.X * p.X, Y: 10 * p.Y, Z: 12 * p.Z * p.Z * p.Z} }, hessian: func(p Vec) *Mat { return NewMat([]float64{ 24 * p.X, 0, 0, 0, 10, 0, 0, 0, 36 * p.Z * p.Z, }) }, }, { field: func(p Vec) float64 { // cos(x) * sin(z) * y^4 y2 := p.Y * p.Y return math.Cos(p.X) * math.Sin(p.Z) * y2 * y2 }, gradient: func(p Vec) Vec { y3 := p.Y * p.Y * p.Y y4 := p.Y * y3 sx, cx := math.Sincos(p.X) sz, cz := math.Sincos(p.Z) return Vec{X: -y4 * sx * sz, Y: 4 * y3 * cx * sz, Z: y4 * cx * cz} }, hessian: func(p Vec) *Mat { y3 := p.Y * p.Y * p.Y y4 := y3 * p.Y sx, cx := math.Sincos(p.X) sz, cz := math.Sincos(p.Z) return NewMat([]float64{ -y4 * cx * sz, -4 * y3 * sx * sz, -y4 * sx * cz, -4 * y3 * sx * sz, 12 * p.Y * p.Y * cx * sz, 4 * y3 * cx * cz, -y4 * sx * cz, 4 * y3 * cx * cz, -y4 * cx * sz, }) }, }, } func TestMatHessian(t *testing.T) { const ( tol = 1e-5 h = 8e-4 ) step := Vec{X: h, Y: h, Z: h} rnd := rand.New(rand.NewSource(1)) for _, test := range scalarFields { for i := 0; i < 30; i++ { p := randomVec(rnd) got := NewMat(nil) got.Hessian(p, step, test.field) want := test.hessian(p) if !mat.EqualApprox(got, want, tol) { t.Errorf("matrices not equal within tol\ngot: %v\nwant: %v", mat.Formatted(got), mat.Formatted(want)) } } } } func TestMatJacobian(t *testing.T) { const ( tol = 1e-5 h = 8e-4 ) step := Vec{X: h, Y: h, Z: h} rnd := rand.New(rand.NewSource(1)) for _, test := range vectorFields { for i := 0; i < 1; i++ { p := randomVec(rnd) got := NewMat(nil) got.Jacobian(p, step, test.field) want := test.jacobian(p) if !mat.EqualApprox(got, want, tol) { t.Errorf("matrices not equal within tol\ngot: %v\nwant: %v", mat.Formatted(got), mat.Formatted(want)) } } } }