spatial/curve: new package to constuct 2-, 3- and 4-D Hilbert curves

diff --git a/spatial/curve/doc.go b/spatial/curve/doc.go
new file mode 100644
index 0000000..847c4cf
--- /dev/null
+++ b/spatial/curve/doc.go
@@ -0,0 +1,38 @@
+// Copyright ©2024 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 curve defines space filling curves. A space filling curve is a curve
+// whose range contains the entirety of a finite k-dimensional space. Space
+// filling curves can be used to map between a linear space and a 2D, 3D, or 4D
+// space.
+//
+// # Hilbert Curves
+//
+// The Hilbert curve is a continuous, space filling curve first described by
+// David Hilbert. The implementation of Hilbert2D is based on example code from
+// the [Hilbert curve (Wikipedia)]. The implementation of Hilbert3D and
+// Hilbert4D are extrapolated from Hilbert2D.
+//
+// Technically, a Hilbert curve is a continuous, fractal, space-filling curve of
+// infinite length, constructed as the limit of a series of piecewise linear
+// curves. We refer to the kth piecewise linear curve as the k-order Hilbert
+// curve. The first-order 2D Hilbert curve looks like ⊐. The k-order
+// n-dimensional curve can be constructed from 2ⁿ copies of the (k-1) order
+// curve. These (finite) Hilbert curves are iterative/recursive, and the order
+// refers to the iteration number.
+//
+// The runtime of Space and Curve scales as O(n∙k) where n is the dimension and
+// k is the order. The length of the curve is 2^(n∙k).
+//
+// # Limitations
+//
+// An n-dimensional, k-order Hilbert curve will not be fully usable if 2ⁿᵏ
+// overflows int (which is dependent on architecture). Len will overflow if n∙k
+// ≥ bits.UintSize-1. Curve will overflow if n∙k > bits.UintSize-1 for some
+// values of v. Space will not overflow, but it cannot be called with values
+// that do not fit in a signed integer, thus only a subset of the curve can be
+// utilized.
+//
+// [Hilbert curve (Wikipedia)]: https://en.wikipedia.org/w/index.php?title=Hilbert_curve&oldid=1011599190
+package curve
diff --git a/spatial/curve/hilbert.go b/spatial/curve/hilbert.go
new file mode 100644
index 0000000..9edecfe
--- /dev/null
+++ b/spatial/curve/hilbert.go
@@ -0,0 +1,334 @@
+// Copyright ©2024 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 curve
+
+import (
+	"errors"
+	"fmt"
+)
+
+// ErrUnderflow is returned by Hilbert curve constructors when the power is less
+// than 1.
+var ErrUnderflow = errors.New("order is less than 1")
+
+// ErrOverflow is returned (wrapped) by Hilbert curve constructors when the
+// power would cause Len and Pos to overflow.
+var ErrOverflow = errors.New("overflow int")
+
+// The size of an int. Taken from src/math/const.go.
+const intSize = 32 << (^uint(0) >> 63) // 32 or 64
+
+// Hilbert2D is a 2-dimensional Hilbert curve.
+type Hilbert2D struct{ order int }
+
+// NewHilbert2D constructs a [Hilbert2D] of the given order. NewHilbert2D
+// returns [ErrOverflow] (wrapped) if the order would cause Len and Pos to
+// overflow.
+func NewHilbert2D(order int) (Hilbert2D, error) {
+	v := Hilbert2D{order: order}
+
+	// The order must be greater than zero.
+	if order < 1 {
+		return v, ErrUnderflow
+	}
+
+	// The product of the order and number of dimensions must not exceed or
+	// equal the size of an int.
+	if order*2 >= intSize {
+		return v, fmt.Errorf("a 2-dimensional, %d-order Hilbert curve will %w", order, ErrOverflow)
+	}
+
+	return v, nil
+}
+
+// Dims returns the spatial dimensions of the curve, which is {2ᵏ, 2ᵏ}, where k
+// is the order.
+func (h Hilbert2D) Dims() []int { return []int{1 << h.order, 1 << h.order} }
+
+// Len returns the length of the curve, which is 2ⁿᵏ, where n is the dimension
+// (2) and k is the order.
+//
+// Len will overflow if order is ≥ 16 on architectures where [int] is 32-bits,
+// or ≥ 32 on architectures where [int] is 64-bits.
+func (h Hilbert2D) Len() int { return 1 << (2 * h.order) }
+
+func (h Hilbert2D) rot(n int, v []int, d int) {
+	switch d {
+	case 0:
+		swap{0, 1}.do(n, v)
+	case 3:
+		flip{0, 1}.do(n, v)
+	}
+}
+
+// Pos returns the linear position of the 3-spatial coordinate v along the
+// curve. Pos modifies v.
+//
+// Pos will overflow if order is ≥ 16 on architectures where [int] is 32-bits,
+// or ≥ 32 on architectures where [int] is 64-bits.
+func (h Hilbert2D) Pos(v []int) int {
+	var d int
+	const dims = 2
+	for n := h.order - 1; n >= 0; n-- {
+		rx := (v[0] >> n) & 1
+		ry := (v[1] >> n) & 1
+		rd := ry<<1 | (ry ^ rx)
+		d += rd << (dims * n)
+		h.rot(h.order, v, rd)
+	}
+	return d
+}
+
+// Coord writes the spatial coordinates of pos to dst and returns it. If dst is
+// nil, Coord allocates a new slice of length 2; otherwise Coord is
+// allocation-free.
+//
+// Coord panics if dst is not nil and len(dst) is not equal to 2.
+func (h Hilbert2D) Coord(dst []int, pos int) []int {
+	if dst == nil {
+		dst = make([]int, 2)
+	} else if len(dst) != 2 {
+		panic("len(dst) must equal 2")
+	}
+	for n := 0; n < h.order; n++ {
+		e := pos & 3
+		h.rot(n, dst[:], e)
+
+		ry := e >> 1
+		rx := (e>>0 ^ e>>1) & 1
+		dst[0] += rx << n
+		dst[1] += ry << n
+		pos >>= 2
+	}
+	return dst
+}
+
+// Hilbert3D is a 3-dimensional Hilbert curve.
+type Hilbert3D struct{ order int }
+
+// NewHilbert3D constructs a [Hilbert3D] of the given order. NewHilbert3D
+// returns [ErrOverflow] (wrapped) if the order would cause Len and Pos to
+// overflow.
+func NewHilbert3D(order int) (Hilbert3D, error) {
+	v := Hilbert3D{order: order}
+
+	// The order must be greater than zero.
+	if order < 1 {
+		return v, ErrUnderflow
+	}
+
+	// The product of the order and number of dimensions must not exceed or
+	// equal the size of an int.
+	if order*3 >= intSize {
+		return v, fmt.Errorf("a 3-dimensional, %d-order Hilbert curve will %w", order, ErrOverflow)
+	}
+
+	return v, nil
+}
+
+// Dims returns the spatial dimensions of the curve, which is {2ᵏ, 2ᵏ, 2ᵏ}, where
+// k is the order.
+func (h Hilbert3D) Dims() []int { return []int{1 << h.order, 1 << h.order, 1 << h.order} }
+
+// Len returns the length of the curve, which is 2ⁿᵏ, where n is the dimension
+// (3) and k is the order.
+//
+// Len will overflow if order is ≥ 11 on architectures where [int] is 32-bits,
+// or ≥ 21 on architectures where [int] is 64-bits.
+func (h Hilbert3D) Len() int { return 1 << (3 * h.order) }
+
+func (h Hilbert3D) rot(reverse bool, n int, v []int, d int) {
+	switch d {
+	case 0:
+		do2(reverse, n, v, swap{1, 2}, swap{0, 2})
+	case 1, 2:
+		do2(reverse, n, v, swap{0, 2}, swap{1, 2})
+	case 3, 4:
+		invert{0, 1}.do(n, v)
+	case 5, 6:
+		do2(reverse, n, v, flip{0, 2}, flip{1, 2})
+	case 7:
+		do2(reverse, n, v, flip{1, 2}, flip{0, 2})
+	}
+}
+
+// Pos returns the linear position of the 4-spatial coordinate v along the
+// curve. Pos modifies v.
+//
+// Pos will overflow if order is ≥ 11 on architectures where [int] is 32-bits,
+// or ≥ 21 on architectures where [int] is 64-bits.
+func (h Hilbert3D) Pos(v []int) int {
+	var d int
+	const dims = 3
+	for n := h.order - 1; n >= 0; n-- {
+		rx := (v[0] >> n) & 1
+		ry := (v[1] >> n) & 1
+		rz := (v[2] >> n) & 1
+		rd := rz<<2 | (rz^ry)<<1 | (rz ^ ry ^ rx)
+		d += rd << (dims * n)
+		h.rot(false, h.order, v, rd)
+	}
+	return d
+}
+
+// Coord writes the spatial coordinates of pos to dst and returns it. If dst is
+// nil, Coord allocates a new slice of length 3; otherwise Coord is
+// allocation-free.
+//
+// Coord panics if dst is not nil and len(dst) is not equal to 3.
+func (h Hilbert3D) Coord(dst []int, pos int) []int {
+	if dst == nil {
+		dst = make([]int, 3)
+	} else if len(dst) != 3 {
+		panic("len(dst) must equal 3")
+	}
+	for n := 0; n < h.order; n++ {
+		e := pos & 7
+		h.rot(true, n, dst[:], e)
+
+		rz := e >> 2
+		ry := (e>>1 ^ e>>2) & 1
+		rx := (e>>0 ^ e>>1) & 1
+		dst[0] += rx << n
+		dst[1] += ry << n
+		dst[2] += rz << n
+		pos >>= 3
+	}
+	return dst
+}
+
+// Hilbert4D is a 4-dimensional Hilbert curve.
+type Hilbert4D struct{ order int }
+
+// NewHilbert4D constructs a [Hilbert4D] of the given order. NewHilbert4D
+// returns [ErrOverflow] (wrapped) if the order would cause Len and Pos to
+// overflow.
+func NewHilbert4D(order int) (Hilbert4D, error) {
+	v := Hilbert4D{order: order}
+
+	// The order must be greater than zero.
+	if order < 1 {
+		return v, ErrUnderflow
+	}
+
+	// The product of the order and number of dimensions must not exceed or
+	// equal the size of an int.
+	if order*4 >= intSize {
+		return v, fmt.Errorf("a 4-dimensional, %d-order Hilbert curve will %w", order, ErrOverflow)
+	}
+
+	return v, nil
+}
+
+// Dims returns the spatial dimensions of the curve, which is {2ᵏ, 2ᵏ, 2ᵏ, 2ᵏ},
+// where k is the order.
+func (h Hilbert4D) Dims() []int { return []int{1 << h.order, 1 << h.order, 1 << h.order, 1 << h.order} }
+
+// Len returns the length of the curve, which is 2ⁿᵏ, where n is the dimension
+// (4) and k is the order.
+//
+// Len will overflow if order is ≥ 8 on architectures where [int] is 32-bits, or
+// ≥ 16 on architectures where [int] is 64-bits.
+func (h Hilbert4D) Len() int { return 1 << (4 * h.order) }
+
+func (h Hilbert4D) rot(reverse bool, n int, v []int, d int) {
+	switch d {
+	case 0:
+		do2(reverse, n, v, swap{1, 3}, swap{0, 3})
+	case 1, 2:
+		do2(reverse, n, v, swap{0, 3}, swap{1, 3})
+	case 3, 4:
+		do2(reverse, n, v, flip{0, 1}, swap{2, 3})
+	case 5, 6:
+		do2(reverse, n, v, flip{1, 2}, swap{2, 3})
+	case 7, 8:
+		invert{0, 2}.do(n, v)
+	case 9, 10:
+		do2(reverse, n, v, flip{1, 2}, flip{2, 3})
+	case 11, 12:
+		do2(reverse, n, v, flip{0, 1}, flip{2, 3})
+	case 13, 14:
+		do2(reverse, n, v, flip{0, 3}, flip{1, 3})
+	case 15:
+		do2(reverse, n, v, flip{1, 3}, flip{0, 3})
+	}
+}
+
+// Pos returns the linear position of the 2-spatial coordinate v along the
+// curve. Pos modifies v.
+//
+// Pos will overflow if order is ≥ 8 on architectures where [int] is 32-bits, or
+// ≥ 16 on architectures where [int] is 64-bits.
+func (h Hilbert4D) Pos(v []int) int {
+	var d int
+	const dims = 4
+	for n := h.order - 1; n >= 0; n-- {
+		var e int
+		for i := dims - 1; i >= 0; i-- {
+			v := v[i] >> n & 1
+			e = e<<1 | (e^v)&1
+		}
+
+		d += e << (dims * n)
+		h.rot(false, h.order, v, e)
+	}
+	return d
+}
+
+// Coord writes the spatial coordinates of pos to dst and returns it. If dst is
+// nil, Coord allocates a new slice of length 4; otherwise Coord is
+// allocation-free.
+//
+// Coord panics if dst is not nil and len(dst) is not equal to 4.
+func (h Hilbert4D) Coord(dst []int, pos int) []int {
+	if dst == nil {
+		dst = make([]int, 4)
+	} else if len(dst) != 4 {
+		panic("len(dst) must equal 4")
+	}
+	N := 4
+	for n := 0; n < h.order; n++ {
+		e := pos & (1<<N - 1)
+		h.rot(true, n, dst[:], e)
+
+		for i, e := 0, e; i < N; i++ {
+			dst[i] += (e ^ e>>1) & 1 << n
+			e >>= 1
+		}
+		pos >>= N
+	}
+	return dst
+}
+
+type op interface{ do(int, []int) }
+
+// invert I and J
+type invert struct{ i, j int }
+
+func (c invert) do(n int, v []int) { v[c.i], v[c.j] = v[c.i]^(1<<n-1), v[c.j]^(1<<n-1) }
+
+// swap I and J
+type swap struct{ i, j int }
+
+func (c swap) do(n int, v []int) { v[c.i], v[c.j] = v[c.j], v[c.i] }
+
+// swap and invert I and J
+type flip struct{ i, j int }
+
+func (c flip) do(n int, v []int) { v[c.i], v[c.j] = v[c.j]^(1<<n-1), v[c.i]^(1<<n-1) }
+
+// do2 executes the given operations, optionally in reverse.
+//
+// Generic specialization reduces allocation (because it can eliminate interface
+// value boxing) and improves performance
+func do2[A, B op](reverse bool, n int, v []int, a A, b B) {
+	if reverse {
+		b.do(n, v)
+		a.do(n, v)
+	} else {
+		a.do(n, v)
+		b.do(n, v)
+	}
+}
diff --git a/spatial/curve/hilbert_test.go b/spatial/curve/hilbert_test.go
new file mode 100644
index 0000000..b68b14e
--- /dev/null
+++ b/spatial/curve/hilbert_test.go
@@ -0,0 +1,457 @@
+// Copyright ©2024 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 curve
+
+import (
+	"errors"
+	"fmt"
+	"reflect"
+	"slices"
+	"strings"
+	"testing"
+
+	"golang.org/x/exp/rand"
+)
+
+func ExampleHilbert2D_Pos() {
+	h := Hilbert2D{order: 3}
+
+	for y := 0; y < 1<<h.order; y++ {
+		for x := 0; x < 1<<h.order; x++ {
+			if x > 0 {
+				fmt.Print("  ")
+			}
+			fmt.Printf("%02x", h.Pos([]int{x, y}))
+		}
+		fmt.Println()
+	}
+	// Output:
+	// 00  01  0e  0f  10  13  14  15
+	// 03  02  0d  0c  11  12  17  16
+	// 04  07  08  0b  1e  1d  18  19
+	// 05  06  09  0a  1f  1c  1b  1a
+	// 3a  39  36  35  20  23  24  25
+	// 3b  38  37  34  21  22  27  26
+	// 3c  3d  32  33  2e  2d  28  29
+	// 3f  3e  31  30  2f  2c  2b  2a
+}
+
+func ExampleHilbert3D_Pos() {
+	h := Hilbert3D{order: 2}
+
+	for z := 0; z < 1<<h.order; z++ {
+		for y := 0; y < 1<<h.order; y++ {
+			for x := 0; x < 1<<h.order; x++ {
+				if x > 0 {
+					fmt.Print("  ")
+				}
+				fmt.Printf("%02x", h.Pos([]int{x, y, z}))
+			}
+			fmt.Println()
+		}
+		fmt.Println()
+	}
+	// Output:
+	// 00  07  08  0b
+	// 01  06  0f  0c
+	// 1a  1b  10  13
+	// 19  18  17  14
+	//
+	// 03  04  09  0a
+	// 02  05  0e  0d
+	// 1d  1c  11  12
+	// 1e  1f  16  15
+	//
+	// 3c  3b  36  35
+	// 3d  3a  31  32
+	// 22  23  2e  2d
+	// 21  20  29  2a
+	//
+	// 3f  38  37  34
+	// 3e  39  30  33
+	// 25  24  2f  2c
+	// 26  27  28  2b
+}
+
+func ExampleHilbert4D_Pos() {
+	h := Hilbert4D{order: 2}
+	N := 1 << h.order
+	for z := 0; z < N; z++ {
+		if z > 0 {
+			s := strings.Repeat("═", N*4-2)
+			s = s + strings.Repeat("═╬═"+s, N-1)
+			fmt.Println(s)
+		}
+		for y := 0; y < N; y++ {
+			for w := 0; w < N; w++ {
+				if w > 0 {
+					fmt.Print(" ║ ")
+				}
+				for x := 0; x < N; x++ {
+					if x > 0 {
+						fmt.Print("  ")
+					}
+					fmt.Printf("%02x", h.Pos([]int{x, y, z, w}))
+				}
+			}
+			fmt.Println()
+		}
+	}
+	// Output:
+	// 00  0f  10  13 ║ 03  0c  11  12 ║ fc  f3  ee  ed ║ ff  f0  ef  ec
+	// 01  0e  1f  1c ║ 02  0d  1e  1d ║ fd  f2  e1  e2 ║ fe  f1  e0  e3
+	// 32  31  20  23 ║ 35  36  21  22 ║ ca  c9  de  dd ║ cd  ce  df  dc
+	// 33  30  2f  2c ║ 34  37  2e  2d ║ cb  c8  d1  d2 ║ cc  cf  d0  d3
+	// ═══════════════╬════════════════╬════════════════╬═══════════════
+	// 07  08  17  14 ║ 04  0b  16  15 ║ fb  f4  e9  ea ║ f8  f7  e8  eb
+	// 06  09  18  1b ║ 05  0a  19  1a ║ fa  f5  e6  e5 ║ f9  f6  e7  e4
+	// 3d  3e  27  24 ║ 3a  39  26  25 ║ c5  c6  d9  da ║ c2  c1  d8  db
+	// 3c  3f  28  2b ║ 3b  38  29  2a ║ c4  c7  d6  d5 ║ c3  c0  d7  d4
+	// ═══════════════╬════════════════╬════════════════╬═══════════════
+	// 76  77  6c  6d ║ 79  78  6b  6a ║ 86  87  94  95 ║ 89  88  93  92
+	// 75  74  63  62 ║ 7a  7b  64  65 ║ 85  84  9b  9a ║ 8a  8b  9c  9d
+	// 42  41  5c  5d ║ 45  46  5b  5a ║ ba  b9  a4  a5 ║ bd  be  a3  a2
+	// 43  40  53  52 ║ 44  47  54  55 ║ bb  b8  ab  aa ║ bc  bf  ac  ad
+	// ═══════════════╬════════════════╬════════════════╬═══════════════
+	// 71  70  6f  6e ║ 7e  7f  68  69 ║ 81  80  97  96 ║ 8e  8f  90  91
+	// 72  73  60  61 ║ 7d  7c  67  66 ║ 82  83  98  99 ║ 8d  8c  9f  9e
+	// 4d  4e  5f  5e ║ 4a  49  58  59 ║ b5  b6  a7  a6 ║ b2  b1  a0  a1
+	// 4c  4f  50  51 ║ 4b  48  57  56 ║ b4  b7  a8  a9 ║ b3  b0  af  ae
+}
+
+func TestConstructors(t *testing.T) {
+	const intSize = 32 << (^uint(0) >> 63) // 32 or 64
+
+	t.Run("2D/Ok", func(t *testing.T) {
+		_, err := NewHilbert2D(intSize/2 - 1)
+		noError(t, err)
+	})
+
+	t.Run("3D/Ok", func(t *testing.T) {
+		_, err := NewHilbert3D(intSize / 3)
+		noError(t, err)
+	})
+
+	t.Run("4D/Ok", func(t *testing.T) {
+		_, err := NewHilbert4D(intSize/4 - 1)
+		noError(t, err)
+	})
+
+	t.Run("2D/Underflow", func(t *testing.T) {
+		_, err := NewHilbert2D(0)
+		errorIs(t, err, ErrUnderflow)
+	})
+
+	t.Run("3D/Underflow", func(t *testing.T) {
+		_, err := NewHilbert3D(0)
+		errorIs(t, err, ErrUnderflow)
+	})
+
+	t.Run("4D/Underflow", func(t *testing.T) {
+		_, err := NewHilbert4D(0)
+		errorIs(t, err, ErrUnderflow)
+	})
+
+	t.Run("2D/Overflow", func(t *testing.T) {
+		_, err := NewHilbert2D(intSize / 2)
+		errorIs(t, err, ErrOverflow)
+	})
+
+	t.Run("3D/Overflow", func(t *testing.T) {
+		_, err := NewHilbert3D(intSize/3 + 1)
+		errorIs(t, err, ErrOverflow)
+	})
+
+	t.Run("4D/Overflow", func(t *testing.T) {
+		_, err := NewHilbert4D(intSize / 4)
+		errorIs(t, err, ErrOverflow)
+	})
+}
+
+func TestHilbert2D(t *testing.T) {
+	for ord := 1; ord <= 4; ord++ {
+		t.Run(fmt.Sprintf("Order/%d", ord), func(t *testing.T) {
+			testCurve(t, Hilbert2D{order: ord})
+		})
+	}
+
+	cases := map[int][]int{
+		1: {
+			0, 1,
+			3, 2,
+		},
+		2: {
+			0x0, 0x3, 0x4, 0x5,
+			0x1, 0x2, 0x7, 0x6,
+			0xE, 0xD, 0x8, 0x9,
+			0xF, 0xC, 0xB, 0xA,
+		},
+		3: {
+			0x00, 0x01, 0x0E, 0x0F, 0x10, 0x13, 0x14, 0x15,
+			0x03, 0x02, 0x0D, 0x0C, 0x11, 0x12, 0x17, 0x16,
+			0x04, 0x07, 0x08, 0x0B, 0x1E, 0x1D, 0x18, 0x19,
+			0x05, 0x06, 0x09, 0x0A, 0x1F, 0x1C, 0x1B, 0x1A,
+			0x3A, 0x39, 0x36, 0x35, 0x20, 0x23, 0x24, 0x25,
+			0x3B, 0x38, 0x37, 0x34, 0x21, 0x22, 0x27, 0x26,
+			0x3C, 0x3D, 0x32, 0x33, 0x2E, 0x2D, 0x28, 0x29,
+			0x3F, 0x3E, 0x31, 0x30, 0x2F, 0x2C, 0x2B, 0x2A,
+		},
+	}
+
+	for order, expected := range cases {
+		t.Run(fmt.Sprintf("Case/%d", order), func(t *testing.T) {
+			testCurveCase(t, Hilbert2D{order: order}, order, expected)
+		})
+	}
+}
+
+func TestHilbert3D(t *testing.T) {
+	for ord := 1; ord <= 4; ord++ {
+		t.Run(fmt.Sprintf("Order/%d", ord), func(t *testing.T) {
+			testCurve(t, Hilbert3D{order: ord})
+		})
+	}
+
+	cases := map[int][]int{
+		1: {
+			0, 1,
+			3, 2,
+
+			7, 6,
+			4, 5,
+		},
+		2: {
+			0x00, 0x07, 0x08, 0x0B,
+			0x01, 0x06, 0x0F, 0x0C,
+			0x1A, 0x1B, 0x10, 0x13,
+			0x19, 0x18, 0x17, 0x14,
+
+			0x03, 0x04, 0x09, 0x0A,
+			0x02, 0x05, 0x0E, 0x0D,
+			0x1D, 0x1C, 0x11, 0x12,
+			0x1E, 0x1F, 0x16, 0x15,
+
+			0x3C, 0x3B, 0x36, 0x35,
+			0x3D, 0x3A, 0x31, 0x32,
+			0x22, 0x23, 0x2E, 0x2D,
+			0x21, 0x20, 0x29, 0x2A,
+
+			0x3F, 0x38, 0x37, 0x34,
+			0x3E, 0x39, 0x30, 0x33,
+			0x25, 0x24, 0x2F, 0x2C,
+			0x26, 0x27, 0x28, 0x2B,
+		},
+	}
+
+	for order, expected := range cases {
+		t.Run(fmt.Sprintf("Case/%d", order), func(t *testing.T) {
+			testCurveCase(t, Hilbert3D{order: order}, order, expected)
+		})
+	}
+}
+
+func TestHilbert4D(t *testing.T) {
+	for ord := 1; ord <= 4; ord++ {
+		t.Run(fmt.Sprintf("Order %d", ord), func(t *testing.T) {
+			testCurve(t, Hilbert4D{order: ord})
+		})
+	}
+}
+
+func BenchmarkHilbert(b *testing.B) {
+	const O = 10
+	for N := 2; N <= 4; N++ {
+		b.Run(fmt.Sprintf("%dD/Pos", N), func(b *testing.B) {
+			for ord := 1; ord <= O; ord++ {
+				b.Run(fmt.Sprintf("Order %d", ord), func(b *testing.B) {
+					h := newCurve(ord, N)
+					v := make([]int, N)
+					for i := range v {
+						v[i] = rand.Intn(1 << ord)
+					}
+					u := make([]int, N)
+					for n := 0; n < b.N; n++ {
+						copy(u, v)
+						h.Pos(u)
+					}
+				})
+			}
+		})
+
+		b.Run(fmt.Sprintf("%dD/Coord", N), func(b *testing.B) {
+			for ord := 1; ord <= O; ord++ {
+				b.Run(fmt.Sprintf("Order %d", ord), func(b *testing.B) {
+					h := newCurve(ord, N)
+					d := rand.Intn(1 << (ord * N))
+					v := make([]int, N)
+					for n := 0; n < b.N; n++ {
+						h.Coord(v, d)
+					}
+				})
+			}
+		})
+	}
+}
+
+type curve interface {
+	Dims() []int
+	Len() int
+	Pos(v []int) int
+	Coord(dst []int, pos int) []int
+}
+
+func newCurve(order, dim int) curve {
+	switch dim {
+	case 2:
+		return Hilbert2D{order: order}
+	case 3:
+		return Hilbert3D{order: order}
+	case 4:
+		return Hilbert4D{order: order}
+	}
+	panic("invalid dimension")
+}
+
+// testCurve verifies that Pos and Coord (of C) are inverses of each other and
+// that the spatial coordinates V and U - corresponding to linear coordinates D
+// and D+1 - are exactly one unit (euclidean) distant from each other.
+func testCurve(t *testing.T, c curve) {
+	t.Helper()
+
+	// Stop if the error count reaches 10
+	var errc int
+	fail := func() {
+		if errc < 10 {
+			errc++
+			t.Fail()
+		} else {
+			t.FailNow()
+		}
+	}
+
+	// Map between linear and spatial coordinates, and verify that Pos and Coord
+	// are inverses of each other
+	m := map[int][]int{}
+	curveRange(c, func(v []int) {
+		d := c.Pos(slices.Clone(v))
+		u := c.Coord(nil, d)
+		if !reflect.DeepEqual(v, u) {
+			t.Logf("Space is not the inverse of Curve for d=%d %v", d, v)
+			fail()
+		}
+
+		m[d] = slices.Clone(v)
+	})
+
+	D := 1
+	for _, v := range c.Dims() {
+		D *= v
+	}
+
+	// For each possible pairs of linear coordinates D and D+1, verify that the
+	// corresponding spatial coordinates V and U are exactly one unit apart
+	// (euclidean distance)
+	for d := 0; d < D-1; d++ {
+		v, u := m[d], m[d+1]
+		if !adjacent(v, u) {
+			t.Logf("points %x and %x are not adjacent\n    %v -> %v", d, d+1, v, u)
+			fail()
+		}
+	}
+}
+
+// curveRange ranges over the n-dimensional coordinate space of the curve,
+// calling fn on each element of the space.
+func curveRange(c curve, fn func([]int)) {
+	size := c.Dims()
+	dimRange(len(size), size, make([]int, len(size)), fn)
+}
+
+// dimRange ranges over the coordinate space defined by size, calling fn on each
+// element of the space. Call dimRange with dim = len(size).
+func dimRange(dim int, size []int, v []int, fn func([]int)) {
+	if dim == 0 {
+		fn(v)
+		return
+	}
+
+	for i := 0; i < size[dim-1]; i++ {
+		v[dim-1] = i
+		dimRange(dim-1, size, v, fn)
+	}
+}
+
+// adjacent returns true if the euclidean distance between v and u is
+// exactly one. v and u must be the same length.
+//
+// In other words, given d(i) = abs(v[i] - u[i]), adjacent returns false if
+// d(i) > 1 for any i or if d(i) == 1 for more than a single i, or if d(i)
+// == 0 for all i.
+func adjacent(v, u []int) bool {
+	n := 0
+	for i := range v {
+		x := v[i] - u[i]
+		if x == 0 {
+			continue
+		}
+		if x < -1 || 1 < x {
+			return false
+		}
+		n++
+	}
+
+	return n == 1
+}
+
+// testCurveCase verifies that the curve produces the expected sequence of
+// values.
+func testCurveCase(t *testing.T, c curve, order int, expected []int) {
+	t.Helper()
+
+	dim := len(c.Dims())
+	actual := make([]int, len(expected))
+	for i := range expected {
+		v := coord(i, order, dim)
+		actual[i] = c.Pos(slices.Clone(v))
+	}
+	if !reflect.DeepEqual(expected, actual) {
+		t.Fatalf("unexpected result:\ngot:  %v\nwant: %v", actual, expected)
+	}
+
+	for i, d := range expected {
+		v := coord(i, order, dim)
+		if !reflect.DeepEqual(v, c.Coord(nil, d)) {
+			t.Fatalf("[%d] expected %v for d = %d", i, v, d)
+		}
+	}
+}
+
+// coord returns the nth spatial coordinates for a dim-dimensional space with
+// sides 2^ord in row-major order.
+func coord(n, ord, dim int) []int {
+	v := make([]int, dim)
+	for i := 0; i < dim; i++ {
+		v[i] = n % (1 << ord)
+		n /= (1 << ord)
+	}
+	return v
+}
+
+func noError(t testing.TB, err error) {
+	t.Helper()
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func errorIs(t testing.TB, err, target error) {
+	t.Helper()
+	if err == nil {
+		t.Fatal("Expected an error")
+	}
+	if !errors.Is(err, target) {
+		t.Fatalf("Expected %v to be %v", err, target)
+	}
+}