Repository: gonum/gonum
Pull request #1769 (2022-02-11 03:59:06)

graph/formats/rdf: add Has{All,Any}{Out,In} and Repeat query methods
Please take a look.

<!--
Checklist:

- API changes have been discussed
- code is goformated correctly (goimports)
- packages with generated code have had code generation run
- tests pass locally
- linked to relevant issues

Please make sure your commit message summary line and pull request
title match the Go convention; a one-line summary of the change,
prefixed by the primary affected package that should complete the
sentence, "This change modifies Gonum to _____."
-->


codecov-commenter: # [Codecov](https://codecov.io/gh/gonum/gonum/pull/1769?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum) Report
> Merging [#1769](https://codecov.io/gh/gonum/gonum/pull/1769?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum) (75dcd60) into [master](https://codecov.io/gh/gonum/gonum/commit/1f712d5ee0658f85c2b3f561beac3903cea6c4c5?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum) (1f712d5) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/gonum/gonum/pull/1769/graphs/tree.svg?width=650&height=150&src=pr&token=B7qHnqSPNL&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum)](https://codecov.io/gh/gonum/gonum/pull/1769?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum)

```diff
@@           Coverage Diff           @@
##           master    #1769   +/-   ##
=======================================
  Coverage   73.39%   73.40%           
=======================================
  Files         522      522           
  Lines       61677    61694   +17     
=======================================
+ Hits        45268    45285   +17     
  Misses      13854    13854           
  Partials     2555     2555           
```


| [Impacted Files](https://codecov.io/gh/gonum/gonum/pull/1769?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum) | Coverage Δ | |
|---|---|---|
| [graph/formats/rdf/query.go](https://codecov.io/gh/gonum/gonum/pull/1769/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum#diff-Z3JhcGgvZm9ybWF0cy9yZGYvcXVlcnkuZ28=) | `90.81% <100.00%> (+1.92%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/gonum/gonum/pull/1769?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/gonum/gonum/pull/1769?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum). Last update [1f712d5...75dcd60](https://codecov.io/gh/gonum/gonum/pull/1769?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum).

kortschak: This needs `AnyOut` and `AnyIn` to be able to provide the power I want. Don't review yet.
kortschak: You might want to sit down for this one.

Patch:
diff --git a/go.mod b/go.mod
index d16907d2..cfb5fca9 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,5 @@ require (
 	github.com/google/go-cmp v0.5.7
 	golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3
 	golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e
-	gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0
 	gonum.org/v1/plot v0.9.0
 )
diff --git a/go.sum b/go.sum
index 945064b4..ba0f1559 100644
--- a/go.sum
+++ b/go.sum
@@ -71,7 +71,6 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IV
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
 gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
-gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc=
 gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
 gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
 gonum.org/v1/plot v0.9.0 h1:3sEo36Uopv1/SA/dMFFaxXoL5XyikJ9Sf2Vll/k6+2E=
diff --git a/graph/formats/rdf/query.go b/graph/formats/rdf/query.go
index 06758c85..8dd548e0 100644
--- a/graph/formats/rdf/query.go
+++ b/graph/formats/rdf/query.go
@@ -63,6 +63,85 @@ func (q Query) In(fn func(s *Statement) bool) Query {
 	return r
 }
 
+// HasAllOut returns a query holding nodes from the receiver's
+// initial set where all outgoing statements satisfy fn. The
+// query short circuits, so fn is not called after the first
+// failure to match.
+func (q Query) HasAllOut(fn func(s *Statement) bool) Query {
+	r := Query{g: q.g}
+	notFn := not(fn)
+loop:
+	for _, s := range q.terms {
+		it := q.g.From(s.ID())
+		for it.Next() {
+			if ConnectedByAny(q.g.Edge(s.ID(), it.Node().ID()), notFn) {
+				continue loop
+			}
+		}
+		r.terms = append(r.terms, s)
+	}
+	return r
+}
+
+// HasAllIn returns a query holding nodes from the receiver's
+// initial set where all incoming statements satisfy fn. The
+// query short circuits, so fn is not called after the first
+// failure to match.
+func (q Query) HasAllIn(fn func(s *Statement) bool) Query {
+	r := Query{g: q.g}
+	notFn := not(fn)
+loop:
+	for _, s := range q.terms {
+		it := q.g.To(s.ID())
+		for it.Next() {
+			if ConnectedByAny(q.g.Edge(it.Node().ID(), s.ID()), notFn) {
+				continue loop
+			}
+		}
+		r.terms = append(r.terms, s)
+	}
+	return r
+}
+
+// HasAnyOut returns a query holding nodes from the receiver's
+// initial set where any outgoing statements satisfies fn. The
+// query short circuits, so fn is not called after the first match.
+func (q Query) HasAnyOut(fn func(s *Statement) bool) Query {
+	r := Query{g: q.g}
+	for _, s := range q.terms {
+		it := q.g.From(s.ID())
+		for it.Next() {
+			if ConnectedByAny(q.g.Edge(s.ID(), it.Node().ID()), fn) {
+				r.terms = append(r.terms, s)
+				break
+			}
+		}
+	}
+	return r
+}
+
+// HasAnyIn returns a query holding nodes from the receiver's
+// initial set where any incoming statements satisfies fn. The
+// query short circuits, so fn is not called after the first match.
+func (q Query) HasAnyIn(fn func(s *Statement) bool) Query {
+	r := Query{g: q.g}
+	for _, s := range q.terms {
+		it := q.g.To(s.ID())
+		for it.Next() {
+			if ConnectedByAny(q.g.Edge(it.Node().ID(), s.ID()), fn) {
+				r.terms = append(r.terms, s)
+				break
+			}
+		}
+	}
+	return r
+}
+
+// not returns the negation of fn.
+func not(fn func(s *Statement) bool) func(s *Statement) bool {
+	return func(s *Statement) bool { return !fn(s) }
+}
+
 // And returns a query that holds the conjunction of q and p.
 func (q Query) And(p Query) Query {
 	if q.g != p.g {
@@ -153,6 +232,29 @@ func (q Query) Not(p Query) Query {
 	return r
 }
 
+// Repeat repeatedly calls fn on q until the set of results is empty or
+// ok is false, and then returns the result. If the last non-empty result
+// is wanted, fn should return its input and false when the partial
+// traversal returns an empty result.
+//
+// 	result := start.Repeat(func(q rdf.Query) (rdf.Query, bool) {
+// 		r := q.Out(condition)
+// 		if r.Len() == 0 {
+// 			return q, false
+// 		}
+// 		return r, true
+// 	}).Result()
+//
+func (q Query) Repeat(fn func(Query) (q Query, ok bool)) Query {
+	for {
+		var ok bool
+		q, ok = fn(q)
+		if !ok || len(q.terms) == 0 {
+			return q
+		}
+	}
+}
+
 // Unique returns a copy of the receiver that contains only one instance
 // of each term.
 func (q Query) Unique() Query {
@@ -166,6 +268,11 @@ func (q Query) Unique() Query {
 	return r
 }
 
+// Len returns the number of terms held by the query.
+func (q Query) Len() int {
+	return len(q.terms)
+}
+
 // Result returns the terms held by the query.
 func (q Query) Result() []Term {
 	return q.terms
diff --git a/spatial/r3/mat.go b/spatial/r3/mat.go
index 37acc155..850ee65b 100644
--- a/spatial/r3/mat.go
+++ b/spatial/r3/mat.go
@@ -52,8 +52,7 @@ func (m *Mat) Scale(f float64, a mat.Matrix) {
 	}
 }
 
-// Performs matrix multiplication on v:
-//  result = M * v
+// MulVec returns the matrix-vector product M⋅v.
 func (m *Mat) MulVec(v Vec) Vec {
 	if m.data == nil {
 		return Vec{}
@@ -65,8 +64,7 @@ func (m *Mat) MulVec(v Vec) Vec {
 	}
 }
 
-// Performs transposed matrix multiplication on v:
-//  result = Mᵀ * v
+// MulVecTrans returns the matrix-vector product Mᵀ⋅v.
 func (m *Mat) MulVecTrans(v Vec) Vec {
 	if m.data == nil {
 		return Vec{}
@@ -163,3 +161,39 @@ func (m *Mat) VecCol(j int) Vec {
 	}
 	return Vec{X: m.At(0, j), Y: m.At(1, j), Z: m.At(2, j)}
 }
+
+// Outer calculates the outer product of the vectors x and y,
+// where x and y are treated as column vectors, and stores the result in the receiver.
+//  m = alpha * x * yᵀ
+func (m *Mat) Outer(alpha float64, x, y Vec) {
+	ax := alpha * x.X
+	ay := alpha * x.Y
+	az := alpha * x.Z
+	m.Set(0, 0, ax*y.X)
+	m.Set(0, 1, ax*y.Y)
+	m.Set(0, 2, ax*y.Z)
+
+	m.Set(1, 0, ay*y.X)
+	m.Set(1, 1, ay*y.Y)
+	m.Set(1, 2, ay*y.Z)
+
+	m.Set(2, 0, az*y.X)
+	m.Set(2, 1, az*y.Y)
+	m.Set(2, 2, az*y.Z)
+}
+
+// Det calculates the determinant of the receiver using the following formula
+//      ⎡a b c⎤
+//  m = ⎢d e f⎥
+//      ⎣g h i⎦
+//  det(m) = a(ei − fh) − b(di − fg) + c(dh − eg)
+func (m *Mat) Det() float64 {
+	a := m.At(0, 0)
+	b := m.At(0, 1)
+	c := m.At(0, 2)
+
+	deta := m.At(1, 1)*m.At(2, 2) - m.At(1, 2)*m.At(2, 1)
+	detb := m.At(1, 0)*m.At(2, 2) - m.At(1, 2)*m.At(2, 0)
+	detc := m.At(1, 0)*m.At(2, 1) - m.At(1, 1)*m.At(2, 0)
+	return a*deta - b*detb + c*detc
+}
diff --git a/spatial/r3/mat_safe.go b/spatial/r3/mat_safe.go
index ca3e7c5a..89a3250b 100644
--- a/spatial/r3/mat_safe.go
+++ b/spatial/r3/mat_safe.go
@@ -136,3 +136,25 @@ func arrayFrom(vals []float64) *array {
 	copy(a[:], vals)
 	return &a
 }
+
+// Mat returns a 3×3 rotation matrix corresponding to the receiver. It
+// may be used to perform rotations on a 3-vector or to apply the rotation
+// to a 3×n matrix of column vectors. If the receiver is not a unit
+// quaternion, the returned matrix will not be a pure rotation.
+func (r Rotation) Mat() *Mat {
+	w, i, j, k := r.Real, r.Imag, r.Jmag, r.Kmag
+	ii := 2 * i * i
+	jj := 2 * j * j
+	kk := 2 * k * k
+	wi := 2 * w * i
+	wj := 2 * w * j
+	wk := 2 * w * k
+	ij := 2 * i * j
+	jk := 2 * j * k
+	ki := 2 * k * i
+	return &Mat{&array{
+		1 - (jj + kk), ij - wk, ki + wj,
+		ij + wk, 1 - (ii + kk), jk - wi,
+		ki - wj, jk + wi, 1 - (ii + jj),
+	}}
+}
diff --git a/spatial/r3/mat_unsafe.go b/spatial/r3/mat_unsafe.go
index 2281649d..63b6e6bb 100644
--- a/spatial/r3/mat_unsafe.go
+++ b/spatial/r3/mat_unsafe.go
@@ -117,6 +117,28 @@ func (m *Mat) RawMatrix() blas64.General {
 	return blas64.General{Rows: 3, Cols: 3, Data: m.slice(), Stride: 3}
 }
 
+// Mat returns a 3×3 rotation matrix corresponding to the receiver. It
+// may be used to perform rotations on a 3-vector or to apply the rotation
+// to a 3×n matrix of column vectors. If the receiver is not a unit
+// quaternion, the returned matrix will not be a pure rotation.
+func (r Rotation) Mat() *Mat {
+	w, i, j, k := r.Real, r.Imag, r.Jmag, r.Kmag
+	ii := 2 * i * i
+	jj := 2 * j * j
+	kk := 2 * k * k
+	wi := 2 * w * i
+	wj := 2 * w * j
+	wk := 2 * w * k
+	ij := 2 * i * j
+	jk := 2 * j * k
+	ki := 2 * k * i
+	return &Mat{&array{
+		{1 - (jj + kk), ij - wk, ki + wj},
+		{ij + wk, 1 - (ii + kk), jk - wi},
+		{ki - wj, jk + wi, 1 - (ii + jj)},
+	}}
+}
+
 func arrayFrom(vals []float64) *array {
 	// TODO(kortschak): Use array conversion when go1.16 is no longer supported.
 	return (*array)(unsafe.Pointer(&vals[0]))
diff --git a/spatial/r3/rotation.go b/spatial/r3/rotation.go
new file mode 100644
index 00000000..5674b587
--- /dev/null
+++ b/spatial/r3/rotation.go
@@ -0,0 +1,58 @@
+// 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"
+
+	"gonum.org/v1/gonum/num/quat"
+)
+
+// TODO: possibly useful additions to the current rotation API:
+//  - create rotations from Euler angles (NewRotationFromEuler?)
+//  - create rotations from rotation matrices (NewRotationFromMatrix?)
+//  - return the equivalent Euler angles from a Rotation
+//
+// Euler angles have issues (see [1] for a discussion).
+// We should think carefully before adding them in.
+// [1]: http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToEuler/
+
+// Rotation describes a rotation in space.
+type Rotation quat.Number
+
+// NewRotation creates a rotation by alpha, around axis.
+func NewRotation(alpha float64, axis Vec) Rotation {
+	if alpha == 0 {
+		return Rotation{Real: 1}
+	}
+	q := raise(axis)
+	sin, cos := math.Sincos(0.5 * alpha)
+	q = quat.Scale(sin/quat.Abs(q), q)
+	q.Real += cos
+	if len := quat.Abs(q); len != 1 {
+		q = quat.Scale(1/len, q)
+	}
+
+	return Rotation(q)
+}
+
+// Rotate returns p rotated according to the parameters used to construct
+// the receiver.
+func (r Rotation) Rotate(p Vec) Vec {
+	if r.isIdentity() {
+		return p
+	}
+	qq := quat.Number(r)
+	pp := quat.Mul(quat.Mul(qq, raise(p)), quat.Conj(qq))
+	return Vec{X: pp.Imag, Y: pp.Jmag, Z: pp.Kmag}
+}
+
+func (r Rotation) isIdentity() bool {
+	return r == Rotation{Real: 1}
+}
+
+func raise(p Vec) quat.Number {
+	return quat.Number{Imag: p.X, Jmag: p.Y, Kmag: p.Z}
+}
diff --git a/spatial/r3/vector.go b/spatial/r3/vector.go
index 96cdbbf5..da34c8ab 100644
--- a/spatial/r3/vector.go
+++ b/spatial/r3/vector.go
@@ -4,13 +4,7 @@
 
 package r3
 
-import (
-	"math"
-
-	"gonum.org/v1/gonum/blas/blas64"
-	"gonum.org/v1/gonum/mat"
-	"gonum.org/v1/gonum/num/quat"
-)
+import "math"
 
 // Vec is a 3D vector.
 type Vec struct {
@@ -93,95 +87,3 @@ func Cos(p, q Vec) float64 {
 type Box struct {
 	Min, Max Vec
 }
-
-// TODO: possibly useful additions to the current rotation API:
-//  - create rotations from Euler angles (NewRotationFromEuler?)
-//  - create rotations from rotation matrices (NewRotationFromMatrix?)
-//  - return the equivalent Euler angles from a Rotation
-//
-// Euler angles have issues (see [1] for a discussion).
-// We should think carefully before adding them in.
-// [1]: http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToEuler/
-
-// Rotation describes a rotation in space.
-type Rotation quat.Number
-
-// NewRotation creates a rotation by alpha, around axis.
-func NewRotation(alpha float64, axis Vec) Rotation {
-	if alpha == 0 {
-		return Rotation{Real: 1}
-	}
-	q := raise(axis)
-	sin, cos := math.Sincos(0.5 * alpha)
-	q = quat.Scale(sin/quat.Abs(q), q)
-	q.Real += cos
-	if len := quat.Abs(q); len != 1 {
-		q = quat.Scale(1/len, q)
-	}
-
-	return Rotation(q)
-}
-
-// Rotate returns p rotated according to the parameters used to construct
-// the receiver.
-func (r Rotation) Rotate(p Vec) Vec {
-	if r.isIdentity() {
-		return p
-	}
-	qq := quat.Number(r)
-	pp := quat.Mul(quat.Mul(qq, raise(p)), quat.Conj(qq))
-	return Vec{X: pp.Imag, Y: pp.Jmag, Z: pp.Kmag}
-}
-
-func (r Rotation) isIdentity() bool {
-	return r == Rotation{Real: 1}
-}
-
-func raise(p Vec) quat.Number {
-	return quat.Number{Imag: p.X, Jmag: p.Y, Kmag: p.Z}
-}
-
-// Matrix returns a 3×3 rotation matrix corresponding to the receiver. It
-// may be used to perform rotations on a 3-vector or to apply the rotation
-// to a 3×n matrix of column vectors. If the receiver is not a unit
-// quaternion, the returned matrix will not be a pure rotation.
-func (r Rotation) Matrix() mat.Matrix {
-	re, im, jm, km := r.Real, r.Imag, r.Jmag, r.Kmag
-	im2 := im * im
-	jm2 := jm * jm
-	km2 := km * km
-	rim := re * im
-	rjm := re * jm
-	rkm := re * km
-	ijm := im * jm
-	jkm := jm * km
-	kim := km * im
-	return &matrix{
-		1 - 2*(jm2+km2), 2 * (ijm - rkm), 2 * (kim + rjm),
-		2 * (ijm + rkm), 1 - 2*(im2+km2), 2 * (jkm - rim),
-		2 * (kim - rjm), 2 * (jkm + rim), 1 - 2*(im2+jm2),
-	}
-}
-
-// matrix is a 3×3 rotation matrix.
-type matrix [9]float64
-
-var (
-	_ mat.Matrix      = (*matrix)(nil)
-	_ mat.RawMatrixer = (*matrix)(nil)
-)
-
-func (m *matrix) At(i, j int) float64 {
-	if uint(i) >= 3 {
-		panic(mat.ErrRowAccess)
-	}
-	if uint(j) >= 3 {
-		panic(mat.ErrColAccess)
-	}
-	return m[i*3+j]
-}
-func (m *matrix) Dims() (r, c int) { return 3, 3 }
-func (m *matrix) T() mat.Matrix    { return mat.Transpose{Matrix: m} }
-func (m *matrix) RawMatrix() blas64.General {
-	return blas64.General{Rows: 3, Cols: 3, Data: m[:], Stride: 3}
-}


Test patch:
diff --git a/graph/coloring/coloring_test.go b/graph/coloring/coloring_test.go
index cc24a1c9..7af504f3 100644
--- a/graph/coloring/coloring_test.go
+++ b/graph/coloring/coloring_test.go
@@ -454,26 +454,32 @@ func TestDsatur(t *testing.T) {
 }
 
 func TestDsaturExact(t *testing.T) {
+	timeout := time.Microsecond
 	for _, test := range coloringTests {
 		for _, useTimeout := range []bool{false, true} {
 			if test.long && !*runLong && !useTimeout {
 				continue
 			}
-			// Set a backstop to safeguard against occasional long running
-			// cases crashing the entire test set. One minute appears to be
-			// reasonable.
-			timeout := time.Minute
-			if test.long && *runLong {
-				// Allow explicitly long tests all the time they need.
-				timeout = 0
-			}
+			var (
+				term   Terminator
+				cancel func()
+			)
 			if useTimeout {
-				timeout = time.Microsecond
-			}
-			var term Terminator
-			cancel := func() {}
-			if timeout != 0 {
 				term, cancel = context.WithTimeout(context.Background(), timeout)
+			} else {
+				// Set a backstop to safeguard against occasional long running
+				// cases crashing the entire test set so get as much time as we can.
+				deadline, ok := t.Deadline()
+				if ok {
+					// But make sure we are faster than the watchdog.
+					deadline = deadline.Add(-10 * time.Second)
+					if deadline.Before(time.Now()) {
+						t.Errorf("we ran out of time by %q", test.name)
+					}
+					term, cancel = context.WithDeadline(context.Background(), deadline)
+				} else {
+					cancel = func() {}
+				}
 			}
 			k, colors, err := DsaturExact(term, test.g)
 			cancel()
@@ -492,7 +498,11 @@ func TestDsaturExact(t *testing.T) {
 					test.name, xid, yid, colors)
 			}
 			if err != nil && !useTimeout {
-				t.Errorf("unexpected error: %v", err)
+				if err != context.DeadlineExceeded {
+					t.Errorf("unexpected error for %q: %v", test.name, err)
+				} else {
+					t.Logf("test ran too long for %q", test.name)
+				}
 			}
 		}
 	}
diff --git a/graph/formats/rdf/graph_example_test.go b/graph/formats/rdf/graph_example_test.go
index e1f8539f..45ec8127 100644
--- a/graph/formats/rdf/graph_example_test.go
+++ b/graph/formats/rdf/graph_example_test.go
@@ -154,17 +154,15 @@ func ExampleQuery() {
 	}
 
 	// g.V().has('name', 'heracles').repeat(out('father')).emit().values('name')
-	q := heracles
-	for i := 0; ; i++ {
+	var i int
+	heracles.Repeat(func(q rdf.Query) (rdf.Query, bool) {
 		q = q.Out(father)
-		results := q.Out(name).Result()
-		if len(results) == 0 {
-			break
-		}
-		for _, r := range results {
+		for _, r := range q.Out(name).Result() {
 			fmt.Printf("Heracles' lineage %d: %s\n", i, r.Value)
 		}
-	}
+		i++
+		return q, true
+	})
 
 	// parents and typ are helper filters for queries below.
 	parents := func(s *rdf.Statement) bool {
diff --git a/graph/formats/rdf/query_test.go b/graph/formats/rdf/query_test.go
index a853d085..2c1ad617 100644
--- a/graph/formats/rdf/query_test.go
+++ b/graph/formats/rdf/query_test.go
@@ -5,7 +5,9 @@
 package rdf
 
 import (
+	"io"
 	"reflect"
+	"strings"
 	"testing"
 
 	"golang.org/x/exp/rand"
@@ -179,6 +181,72 @@ func TestQueryNot(t *testing.T) {
 	}
 }
 
+func TestQueryRepeat(t *testing.T) {
+	const filterTestGraph = `
+<ex:a> <p:1> <ex:b> .
+<ex:b> <p:1> <ex:c> .
+<ex:c> <p:1> <ex:d> .
+<ex:d> <p:1> <ex:e> .
+<ex:e> <p:1> <ex:f> .
+<ex:a> <p:2> <ex:_b> .
+<ex:b> <p:2> <ex:_c> .
+<ex:c> <p:2> <ex:_d> .
+<ex:d> <p:2> <ex:_e> .
+<ex:e> <p:2> <ex:_f> .
+`
+
+	want := []string{"<ex:a>", "<ex:b>", "<ex:c>", "<ex:d>", "<ex:e>", "<ex:f>"}
+
+	g, err := graphFromStatements(filterTestGraph)
+	if err != nil {
+		t.Fatalf("unexpected error constructing graph: %v", err)
+	}
+	start, ok := g.TermFor("<ex:a>")
+	if !ok {
+		t.Fatal("could not get start term")
+	}
+	for _, limit := range []int{0, 1, 2, 5, 100} {
+		got := []string{}
+		var i int
+		result := g.Query(start).Repeat(func(q Query) (Query, bool) {
+			if i >= limit {
+				return q, false
+			}
+			i++
+			q = q.Out(func(s *Statement) bool {
+				ok := s.Predicate.Value == "<p:1>"
+				if ok {
+					got = append(got, s.Object.Value)
+				}
+				return ok
+			})
+			return q, true
+		}).Unique().Result()
+
+		n := limit
+		if n >= len(want) {
+			n = len(want) - 1
+		}
+		if !reflect.DeepEqual(got, want[1:n+1]) {
+			t.Errorf("unexpected capture for limit=%d: got:%v want:%v",
+				limit, got, want[:n])
+		}
+
+		switch {
+		case limit < len(want):
+			if len(result) == 0 || result[0].Value != want[i] {
+				t.Errorf("unexpected result for limit=%d: got:%v want:%v",
+					limit, result[0], want[i])
+			}
+		default:
+			if len(result) != 0 {
+				t.Errorf("unexpected result for limit=%d: got: %v want:none",
+					limit, result[0])
+			}
+		}
+	}
+}
+
 var uniqueTests = []struct {
 	name string
 	in   []Term
@@ -219,6 +287,377 @@ func TestQueryUnique(t *testing.T) {
 	}
 }
 
+// filterTestGraph is used to test Has*Out and Has*In. It has a symmetry
+// that means that the in an out tests have the same form, just with opposite
+// directions.
+const filterTestGraph = `
+<ex:a> <p:1> <ex:d> .
+<ex:a> <p:2> <ex:f> .
+<ex:b> <p:2> <ex:d> .
+<ex:c> <p:2> <ex:d> .
+<ex:a> <o:n> <ex:d> .
+# symmetry line.
+<ex:e> <p:1> <ex:a> .
+<ex:g> <p:2> <ex:a> .
+<ex:e> <p:2> <ex:b> .
+<ex:e> <p:2> <ex:c> .
+<ex:e> <o:n> <ex:a> .
+`
+
+var hasOutTests = []struct {
+	name    string
+	in      []Term
+	fn      func(*Statement) bool
+	cons    func(q Query) Query
+	wantAll []Term
+	wantAny []Term
+}{
+	{
+		name: "all",
+		in:   []Term{{Value: "<ex:a>"}, {Value: "<ex:b>"}, {Value: "<ex:c>"}},
+		fn:   func(s *Statement) bool { return true },
+		cons: func(q Query) Query {
+			cond := func(s *Statement) bool { return true }
+			return q.Out(cond).In(cond).Unique()
+		},
+		wantAll: []Term{{Value: "<ex:a>"}, {Value: "<ex:b>"}, {Value: "<ex:c>"}},
+		wantAny: []Term{{Value: "<ex:a>"}, {Value: "<ex:b>"}, {Value: "<ex:c>"}},
+	},
+	{
+		name: "none",
+		in:   []Term{{Value: "<ex:a>", UID: 1}, {Value: "<ex:b>", UID: 2}, {Value: "<ex:c>", UID: 3}},
+		fn:   func(s *Statement) bool { return false },
+		cons: func(q Query) Query {
+			cond := func(s *Statement) bool { return false }
+			return q.Out(cond).In(cond).Unique()
+		},
+		wantAll: nil,
+		wantAny: nil,
+	},
+	{
+		name: ". <p:1> .",
+		in:   []Term{{Value: "<ex:a>", UID: 1}, {Value: "<ex:b>", UID: 2}, {Value: "<ex:c>", UID: 3}},
+		fn:   func(s *Statement) bool { return s.Predicate.Value == "<p:1>" },
+		cons: func(q Query) Query {
+			cond1 := func(s *Statement) bool { return s.Predicate.Value == "<p:1>" }
+			cond2 := func(s *Statement) bool { return s.Predicate.Value != "<p:1>" }
+			return q.Out(cond1).In(cond1).Not(q.Out(cond2).In(cond2)).Unique()
+		},
+		wantAll: nil,
+		wantAny: []Term{{Value: "<ex:a>"}},
+	},
+	{
+		name: "!(. <p:1> .)",
+		in:   []Term{{Value: "<ex:a>", UID: 1}, {Value: "<ex:b>", UID: 2}, {Value: "<ex:c>", UID: 3}},
+		fn:   func(s *Statement) bool { return s.Predicate.Value != "<p:1>" },
+		cons: func(q Query) Query {
+			cond1 := func(s *Statement) bool { return s.Predicate.Value != "<p:1>" }
+			cond2 := func(s *Statement) bool { return s.Predicate.Value == "<p:1>" }
+			return q.Out(cond1).In(cond1).Not(q.Out(cond2).In(cond2)).Unique()
+		},
+		wantAll: []Term{{Value: "<ex:b>"}, {Value: "<ex:c>"}},
+		wantAny: []Term{{Value: "<ex:a>"}, {Value: "<ex:b>"}, {Value: "<ex:c>"}},
+	},
+	{
+		name: "!(. <p:2> .)",
+		in:   []Term{{Value: "<ex:a>", UID: 1}, {Value: "<ex:b>", UID: 2}, {Value: "<ex:c>", UID: 3}},
+		fn:   func(s *Statement) bool { return s.Predicate.Value != "<p:2>" },
+		cons: func(q Query) Query {
+			cond1 := func(s *Statement) bool { return s.Predicate.Value != "<p:2>" }
+			cond2 := func(s *Statement) bool { return s.Predicate.Value == "<p:2>" }
+			return q.Out(cond1).In(cond1).Not(q.Out(cond2).In(cond2)).Unique()
+		},
+		wantAll: nil,
+		wantAny: []Term{{Value: "<ex:a>"}},
+	},
+	{
+		name: "!(. <p:2>  <ex:f>)",
+		in:   []Term{{Value: "<ex:a>", UID: 1}, {Value: "<ex:b>", UID: 2}, {Value: "<ex:c>", UID: 3}},
+		fn: func(s *Statement) bool {
+			return s.Predicate.Value != "<p:2>" || (s.Predicate.Value == "<p:2>" && s.Object.Value != "<ex:f>")
+		},
+		cons: func(q Query) Query {
+			cond := func(s *Statement) bool {
+				return s.Predicate.Value == "<p:2>" && s.Object.Value != "<ex:f>"
+			}
+			return q.Out(cond).In(cond).Unique()
+		},
+		wantAll: []Term{{Value: "<ex:b>"}, {Value: "<ex:c>"}},
+		wantAny: []Term{{Value: "<ex:a>"}, {Value: "<ex:b>"}, {Value: "<ex:c>"}},
+	},
+	{
+		name: "!(. <p:2>  !<ex:f>)",
+		in:   []Term{{Value: "<ex:a>", UID: 1}, {Value: "<ex:b>", UID: 2}, {Value: "<ex:c>", UID: 3}},
+		fn: func(s *Statement) bool {
+			return s.Predicate.Value != "<p:2>" || (s.Predicate.Value == "<p:2>" && s.Object.Value == "<ex:f>")
+		},
+		cons: func(q Query) Query {
+			cond := func(s *Statement) bool {
+				return s.Predicate.Value == "<p:2>" && s.Object.Value == "<ex:f>"
+			}
+			return q.Out(cond).In(cond).Unique()
+		},
+		wantAll: []Term{{Value: "<ex:a>"}},
+		wantAny: []Term{{Value: "<ex:a>"}},
+	},
+}
+
+func TestQueryHasAllOut(t *testing.T) {
+	g, err := graphFromStatements(filterTestGraph)
+	if err != nil {
+		t.Fatalf("unexpected error constructing graph: %v", err)
+	}
+	for _, test := range hasOutTests {
+		ids := make(map[string]int64)
+		for i, v := range test.in {
+			term, ok := g.TermFor(v.Value)
+			if !ok {
+				t.Fatalf("unexpected error constructing graph: could not get UID for term: %v", v.Value)
+			}
+			test.in[i].UID = term.UID
+			ids[term.Value] = term.UID
+		}
+		for i, v := range test.wantAll {
+			test.wantAll[i].UID = ids[v.Value]
+		}
+
+		a := Query{g: g, terms: test.in}
+
+		got := a.HasAllOut(test.fn).Result()
+		sortByID(got)
+		sortByID(test.wantAll)
+
+		if !reflect.DeepEqual(got, test.wantAll) {
+			t.Errorf("unexpected result for test %q:\ngot: %v\nwant:%v",
+				test.name, got, test.wantAll)
+		}
+
+		cons := test.cons(a).Result()
+		sortByID(cons)
+		if !reflect.DeepEqual(got, cons) {
+			t.Errorf("unexpected construction result for test %q:\ngot: %v\nwant:%v",
+				test.name, got, cons)
+		}
+	}
+}
+
+func TestQueryHasAnyOut(t *testing.T) {
+	g, err := graphFromStatements(filterTestGraph)
+	if err != nil {
+		t.Fatalf("unexpected error constructing graph: %v", err)
+	}
+	for _, test := range hasOutTests {
+		ids := make(map[string]int64)
+		for i, v := range test.in {
+			term, ok := g.TermFor(v.Value)
+			if !ok {
+				t.Fatalf("unexpected error constructing graph: could not get UID for term: %v", v.Value)
+			}
+			test.in[i].UID = term.UID
+			ids[term.Value] = term.UID
+		}
+		for i, v := range test.wantAny {
+			test.wantAny[i].UID = ids[v.Value]
+		}
+
+		a := Query{g: g, terms: test.in}
+
+		got := a.HasAnyOut(test.fn).Result()
+		sortByID(got)
+		sortByID(test.wantAny)
+
+		if !reflect.DeepEqual(got, test.wantAny) {
+			t.Errorf("unexpected result for test %q:\ngot: %v\nwant:%v",
+				test.name, got, test.wantAny)
+		}
+	}
+}
+
+var hasInTests = []struct {
+	name    string
+	in      []Term
+	fn      func(*Statement) bool
+	cons    func(q Query) Query
+	wantAll []Term
+	wantAny []Term
+}{
+	{
+		name: "all",
+		in:   []Term{{Value: "<ex:a>"}, {Value: "<ex:b>"}, {Value: "<ex:c>"}},
+		fn:   func(s *Statement) bool { return true },
+		cons: func(q Query) Query {
+			cond := func(s *Statement) bool { return true }
+			return q.In(cond).Out(cond).Unique()
+		},
+		wantAll: []Term{{Value: "<ex:a>"}, {Value: "<ex:b>"}, {Value: "<ex:c>"}},
+		wantAny: []Term{{Value: "<ex:a>"}, {Value: "<ex:b>"}, {Value: "<ex:c>"}},
+	},
+	{
+		name: "none",
+		in:   []Term{{Value: "<ex:a>", UID: 1}, {Value: "<ex:b>", UID: 2}, {Value: "<ex:c>", UID: 3}},
+		fn:   func(s *Statement) bool { return false },
+		cons: func(q Query) Query {
+			cond := func(s *Statement) bool { return false }
+			return q.In(cond).Out(cond).Unique()
+		},
+		wantAll: nil,
+		wantAny: nil,
+	},
+	{
+		name: ". <p:1> .",
+		in:   []Term{{Value: "<ex:a>", UID: 1}, {Value: "<ex:b>", UID: 2}, {Value: "<ex:c>", UID: 3}},
+		fn:   func(s *Statement) bool { return s.Predicate.Value == "<p:1>" },
+		cons: func(q Query) Query {
+			cond1 := func(s *Statement) bool { return s.Predicate.Value == "<p:1>" }
+			cond2 := func(s *Statement) bool { return s.Predicate.Value != "<p:1>" }
+			return q.In(cond1).Out(cond1).Not(q.In(cond2).Out(cond2)).Unique()
+		},
+		wantAll: nil,
+		wantAny: []Term{{Value: "<ex:a>"}},
+	},
+	{
+		name: "!(. <p:1> .)",
+		in:   []Term{{Value: "<ex:a>", UID: 1}, {Value: "<ex:b>", UID: 2}, {Value: "<ex:c>", UID: 3}},
+		fn:   func(s *Statement) bool { return s.Predicate.Value != "<p:1>" },
+		cons: func(q Query) Query {
+			cond1 := func(s *Statement) bool { return s.Predicate.Value != "<p:1>" }
+			cond2 := func(s *Statement) bool { return s.Predicate.Value == "<p:1>" }
+			return q.In(cond1).Out(cond1).Not(q.In(cond2).Out(cond2)).Unique()
+		},
+		wantAll: []Term{{Value: "<ex:b>"}, {Value: "<ex:c>"}},
+		wantAny: []Term{{Value: "<ex:a>"}, {Value: "<ex:b>"}, {Value: "<ex:c>"}},
+	},
+	{
+		name: "!(. <p:2> .)",
+		in:   []Term{{Value: "<ex:a>", UID: 1}, {Value: "<ex:b>", UID: 2}, {Value: "<ex:c>", UID: 3}},
+		fn:   func(s *Statement) bool { return s.Predicate.Value != "<p:2>" },
+		cons: func(q Query) Query {
+			cond1 := func(s *Statement) bool { return s.Predicate.Value != "<p:2>" }
+			cond2 := func(s *Statement) bool { return s.Predicate.Value == "<p:2>" }
+			return q.In(cond1).Out(cond1).Not(q.In(cond2).Out(cond2)).Unique()
+		},
+		wantAll: nil,
+		wantAny: []Term{{Value: "<ex:a>"}},
+	},
+	{
+		name: "!(<ex:f> <p:2>  .)",
+		in:   []Term{{Value: "<ex:a>", UID: 1}, {Value: "<ex:b>", UID: 2}, {Value: "<ex:c>", UID: 3}},
+		fn: func(s *Statement) bool {
+			return s.Predicate.Value != "<p:2>" || (s.Predicate.Value == "<p:2>" && s.Subject.Value != "<ex:g>")
+		},
+		cons: func(q Query) Query {
+			cond := func(s *Statement) bool {
+				return s.Predicate.Value == "<p:2>" && s.Subject.Value != "<ex:g>"
+			}
+			return q.In(cond).Out(cond).Unique()
+		},
+		wantAll: []Term{{Value: "<ex:b>"}, {Value: "<ex:c>"}},
+		wantAny: []Term{{Value: "<ex:a>"}, {Value: "<ex:b>"}, {Value: "<ex:c>"}},
+	},
+	{
+		name: "!(!<ex:f> <p:2>  .)",
+		in:   []Term{{Value: "<ex:a>", UID: 1}, {Value: "<ex:b>", UID: 2}, {Value: "<ex:c>", UID: 3}},
+		fn: func(s *Statement) bool {
+			return s.Predicate.Value != "<p:2>" || (s.Predicate.Value == "<p:2>" && s.Subject.Value == "<ex:g>")
+		},
+		cons: func(q Query) Query {
+			cond := func(s *Statement) bool {
+				return s.Predicate.Value == "<p:2>" && s.Subject.Value == "<ex:g>"
+			}
+			return q.In(cond).Out(cond).Unique()
+		},
+		wantAll: []Term{{Value: "<ex:a>"}},
+		wantAny: []Term{{Value: "<ex:a>"}},
+	},
+}
+
+func TestQueryHasAllIn(t *testing.T) {
+	g, err := graphFromStatements(filterTestGraph)
+	if err != nil {
+		t.Fatalf("unexpected error constructing graph: %v", err)
+	}
+	for _, test := range hasInTests {
+		ids := make(map[string]int64)
+		for i, v := range test.in {
+			term, ok := g.TermFor(v.Value)
+			if !ok {
+				t.Fatalf("unexpected error constructing graph: could not get UID for term: %v", v.Value)
+			}
+			test.in[i].UID = term.UID
+			ids[term.Value] = term.UID
+		}
+		for i, v := range test.wantAll {
+			test.wantAll[i].UID = ids[v.Value]
+		}
+
+		a := Query{g: g, terms: test.in}
+
+		got := a.HasAllIn(test.fn).Result()
+		sortByID(got)
+		sortByID(test.wantAll)
+
+		if !reflect.DeepEqual(got, test.wantAll) {
+			t.Errorf("unexpected result for test %q:\ngot: %v\nwant:%v",
+				test.name, got, test.wantAll)
+		}
+
+		cons := test.cons(a).Result()
+		sortByID(cons)
+		if !reflect.DeepEqual(got, cons) {
+			t.Errorf("unexpected construction result for test %q:\ngot: %v\nwant:%v",
+				test.name, got, cons)
+		}
+	}
+}
+
+func TestQueryHasAnyIn(t *testing.T) {
+	g, err := graphFromStatements(filterTestGraph)
+	if err != nil {
+		t.Fatalf("unexpected error constructing graph: %v", err)
+	}
+	for _, test := range hasInTests {
+		ids := make(map[string]int64)
+		for i, v := range test.in {
+			term, ok := g.TermFor(v.Value)
+			if !ok {
+				t.Fatalf("unexpected error constructing graph: could not get UID for term: %v", v.Value)
+			}
+			test.in[i].UID = term.UID
+			ids[term.Value] = term.UID
+		}
+		for i, v := range test.wantAny {
+			test.wantAny[i].UID = ids[v.Value]
+		}
+
+		a := Query{g: g, terms: test.in}
+
+		got := a.HasAnyIn(test.fn).Result()
+		sortByID(got)
+		sortByID(test.wantAny)
+
+		if !reflect.DeepEqual(got, test.wantAny) {
+			t.Errorf("unexpected result for test %q:\ngot: %v\nwant:%v",
+				test.name, got, test.wantAny)
+		}
+	}
+}
+
+func graphFromStatements(s string) (*Graph, error) {
+	g := NewGraph()
+	dec := NewDecoder(strings.NewReader(s))
+	for {
+		s, err := dec.Unmarshal()
+		if err != nil {
+			if err != io.EOF {
+				return nil, err
+			}
+			break
+		}
+		g.AddStatement(s)
+	}
+	return g, nil
+}
+
 func permutedTerms(t []Term, src rand.Source) []Term {
 	rnd := rand.New(src)
 	p := make([]Term, len(t))
diff --git a/spatial/r3/mat_test.go b/spatial/r3/mat_test.go
index eb165b01..5e70c75f 100644
--- a/spatial/r3/mat_test.go
+++ b/spatial/r3/mat_test.go
@@ -5,11 +5,13 @@
 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) {
@@ -154,3 +156,77 @@ func randomVec(rnd *rand.Rand) (v Vec) {
 	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")
+		}
+	}
+}
diff --git a/spatial/r3/vector_test.go b/spatial/r3/vector_test.go
index e24a2171..ad5376b3 100644
--- a/spatial/r3/vector_test.go
+++ b/spatial/r3/vector_test.go
@@ -243,7 +243,7 @@ func TestRotate(t *testing.T) {
 		}
 
 		var gotv mat.VecDense
-		gotv.MulVec(NewRotation(test.alpha, test.axis).Matrix(), vecDense(test.v))
+		gotv.MulVec(NewRotation(test.alpha, test.axis).Mat(), vecDense(test.v))
 		got = vec(gotv)
 		if !vecApproxEqual(got, test.want, tol) {
 			t.Errorf(