gonum.org/v1/gonum@v0.15.1-0.20240517103525-f853624cb1bb/spatial/curve/hilbert_test.go (about)

     1  // Copyright ©2024 The Gonum Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package curve
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"reflect"
    11  	"slices"
    12  	"strings"
    13  	"testing"
    14  
    15  	"golang.org/x/exp/rand"
    16  )
    17  
    18  func ExampleHilbert2D_Pos() {
    19  	h := Hilbert2D{order: 3}
    20  
    21  	for y := 0; y < 1<<h.order; y++ {
    22  		for x := 0; x < 1<<h.order; x++ {
    23  			if x > 0 {
    24  				fmt.Print("  ")
    25  			}
    26  			fmt.Printf("%02x", h.Pos([]int{x, y}))
    27  		}
    28  		fmt.Println()
    29  	}
    30  	// Output:
    31  	// 00  01  0e  0f  10  13  14  15
    32  	// 03  02  0d  0c  11  12  17  16
    33  	// 04  07  08  0b  1e  1d  18  19
    34  	// 05  06  09  0a  1f  1c  1b  1a
    35  	// 3a  39  36  35  20  23  24  25
    36  	// 3b  38  37  34  21  22  27  26
    37  	// 3c  3d  32  33  2e  2d  28  29
    38  	// 3f  3e  31  30  2f  2c  2b  2a
    39  }
    40  
    41  func ExampleHilbert3D_Pos() {
    42  	h := Hilbert3D{order: 2}
    43  
    44  	for z := 0; z < 1<<h.order; z++ {
    45  		for y := 0; y < 1<<h.order; y++ {
    46  			for x := 0; x < 1<<h.order; x++ {
    47  				if x > 0 {
    48  					fmt.Print("  ")
    49  				}
    50  				fmt.Printf("%02x", h.Pos([]int{x, y, z}))
    51  			}
    52  			fmt.Println()
    53  		}
    54  		fmt.Println()
    55  	}
    56  	// Output:
    57  	// 00  07  08  0b
    58  	// 01  06  0f  0c
    59  	// 1a  1b  10  13
    60  	// 19  18  17  14
    61  	//
    62  	// 03  04  09  0a
    63  	// 02  05  0e  0d
    64  	// 1d  1c  11  12
    65  	// 1e  1f  16  15
    66  	//
    67  	// 3c  3b  36  35
    68  	// 3d  3a  31  32
    69  	// 22  23  2e  2d
    70  	// 21  20  29  2a
    71  	//
    72  	// 3f  38  37  34
    73  	// 3e  39  30  33
    74  	// 25  24  2f  2c
    75  	// 26  27  28  2b
    76  }
    77  
    78  func ExampleHilbert4D_Pos() {
    79  	h := Hilbert4D{order: 2}
    80  	N := 1 << h.order
    81  	for z := 0; z < N; z++ {
    82  		if z > 0 {
    83  			s := strings.Repeat("═", N*4-2)
    84  			s = s + strings.Repeat("═╬═"+s, N-1)
    85  			fmt.Println(s)
    86  		}
    87  		for y := 0; y < N; y++ {
    88  			for w := 0; w < N; w++ {
    89  				if w > 0 {
    90  					fmt.Print(" ║ ")
    91  				}
    92  				for x := 0; x < N; x++ {
    93  					if x > 0 {
    94  						fmt.Print("  ")
    95  					}
    96  					fmt.Printf("%02x", h.Pos([]int{x, y, z, w}))
    97  				}
    98  			}
    99  			fmt.Println()
   100  		}
   101  	}
   102  	// Output:
   103  	// 00  0f  10  13 ║ 03  0c  11  12 ║ fc  f3  ee  ed ║ ff  f0  ef  ec
   104  	// 01  0e  1f  1c ║ 02  0d  1e  1d ║ fd  f2  e1  e2 ║ fe  f1  e0  e3
   105  	// 32  31  20  23 ║ 35  36  21  22 ║ ca  c9  de  dd ║ cd  ce  df  dc
   106  	// 33  30  2f  2c ║ 34  37  2e  2d ║ cb  c8  d1  d2 ║ cc  cf  d0  d3
   107  	// ═══════════════╬════════════════╬════════════════╬═══════════════
   108  	// 07  08  17  14 ║ 04  0b  16  15 ║ fb  f4  e9  ea ║ f8  f7  e8  eb
   109  	// 06  09  18  1b ║ 05  0a  19  1a ║ fa  f5  e6  e5 ║ f9  f6  e7  e4
   110  	// 3d  3e  27  24 ║ 3a  39  26  25 ║ c5  c6  d9  da ║ c2  c1  d8  db
   111  	// 3c  3f  28  2b ║ 3b  38  29  2a ║ c4  c7  d6  d5 ║ c3  c0  d7  d4
   112  	// ═══════════════╬════════════════╬════════════════╬═══════════════
   113  	// 76  77  6c  6d ║ 79  78  6b  6a ║ 86  87  94  95 ║ 89  88  93  92
   114  	// 75  74  63  62 ║ 7a  7b  64  65 ║ 85  84  9b  9a ║ 8a  8b  9c  9d
   115  	// 42  41  5c  5d ║ 45  46  5b  5a ║ ba  b9  a4  a5 ║ bd  be  a3  a2
   116  	// 43  40  53  52 ║ 44  47  54  55 ║ bb  b8  ab  aa ║ bc  bf  ac  ad
   117  	// ═══════════════╬════════════════╬════════════════╬═══════════════
   118  	// 71  70  6f  6e ║ 7e  7f  68  69 ║ 81  80  97  96 ║ 8e  8f  90  91
   119  	// 72  73  60  61 ║ 7d  7c  67  66 ║ 82  83  98  99 ║ 8d  8c  9f  9e
   120  	// 4d  4e  5f  5e ║ 4a  49  58  59 ║ b5  b6  a7  a6 ║ b2  b1  a0  a1
   121  	// 4c  4f  50  51 ║ 4b  48  57  56 ║ b4  b7  a8  a9 ║ b3  b0  af  ae
   122  }
   123  
   124  func TestConstructors(t *testing.T) {
   125  	const intSize = 32 << (^uint(0) >> 63) // 32 or 64
   126  
   127  	t.Run("2D/Ok", func(t *testing.T) {
   128  		_, err := NewHilbert2D(intSize/2 - 1)
   129  		noError(t, err)
   130  	})
   131  
   132  	t.Run("3D/Ok", func(t *testing.T) {
   133  		_, err := NewHilbert3D(intSize / 3)
   134  		noError(t, err)
   135  	})
   136  
   137  	t.Run("4D/Ok", func(t *testing.T) {
   138  		_, err := NewHilbert4D(intSize/4 - 1)
   139  		noError(t, err)
   140  	})
   141  
   142  	t.Run("2D/Underflow", func(t *testing.T) {
   143  		_, err := NewHilbert2D(0)
   144  		errorIs(t, err, ErrUnderflow)
   145  	})
   146  
   147  	t.Run("3D/Underflow", func(t *testing.T) {
   148  		_, err := NewHilbert3D(0)
   149  		errorIs(t, err, ErrUnderflow)
   150  	})
   151  
   152  	t.Run("4D/Underflow", func(t *testing.T) {
   153  		_, err := NewHilbert4D(0)
   154  		errorIs(t, err, ErrUnderflow)
   155  	})
   156  
   157  	t.Run("2D/Overflow", func(t *testing.T) {
   158  		_, err := NewHilbert2D(intSize / 2)
   159  		errorIs(t, err, ErrOverflow)
   160  	})
   161  
   162  	t.Run("3D/Overflow", func(t *testing.T) {
   163  		_, err := NewHilbert3D(intSize/3 + 1)
   164  		errorIs(t, err, ErrOverflow)
   165  	})
   166  
   167  	t.Run("4D/Overflow", func(t *testing.T) {
   168  		_, err := NewHilbert4D(intSize / 4)
   169  		errorIs(t, err, ErrOverflow)
   170  	})
   171  }
   172  
   173  func TestHilbert2D(t *testing.T) {
   174  	for ord := 1; ord <= 4; ord++ {
   175  		t.Run(fmt.Sprintf("Order/%d", ord), func(t *testing.T) {
   176  			testCurve(t, Hilbert2D{order: ord})
   177  		})
   178  	}
   179  
   180  	cases := map[int][]int{
   181  		1: {
   182  			0, 1,
   183  			3, 2,
   184  		},
   185  		2: {
   186  			0x0, 0x3, 0x4, 0x5,
   187  			0x1, 0x2, 0x7, 0x6,
   188  			0xE, 0xD, 0x8, 0x9,
   189  			0xF, 0xC, 0xB, 0xA,
   190  		},
   191  		3: {
   192  			0x00, 0x01, 0x0E, 0x0F, 0x10, 0x13, 0x14, 0x15,
   193  			0x03, 0x02, 0x0D, 0x0C, 0x11, 0x12, 0x17, 0x16,
   194  			0x04, 0x07, 0x08, 0x0B, 0x1E, 0x1D, 0x18, 0x19,
   195  			0x05, 0x06, 0x09, 0x0A, 0x1F, 0x1C, 0x1B, 0x1A,
   196  			0x3A, 0x39, 0x36, 0x35, 0x20, 0x23, 0x24, 0x25,
   197  			0x3B, 0x38, 0x37, 0x34, 0x21, 0x22, 0x27, 0x26,
   198  			0x3C, 0x3D, 0x32, 0x33, 0x2E, 0x2D, 0x28, 0x29,
   199  			0x3F, 0x3E, 0x31, 0x30, 0x2F, 0x2C, 0x2B, 0x2A,
   200  		},
   201  	}
   202  
   203  	for order, expected := range cases {
   204  		t.Run(fmt.Sprintf("Case/%d", order), func(t *testing.T) {
   205  			testCurveCase(t, Hilbert2D{order: order}, order, expected)
   206  		})
   207  	}
   208  }
   209  
   210  func TestHilbert3D(t *testing.T) {
   211  	for ord := 1; ord <= 4; ord++ {
   212  		t.Run(fmt.Sprintf("Order/%d", ord), func(t *testing.T) {
   213  			testCurve(t, Hilbert3D{order: ord})
   214  		})
   215  	}
   216  
   217  	cases := map[int][]int{
   218  		1: {
   219  			0, 1,
   220  			3, 2,
   221  
   222  			7, 6,
   223  			4, 5,
   224  		},
   225  		2: {
   226  			0x00, 0x07, 0x08, 0x0B,
   227  			0x01, 0x06, 0x0F, 0x0C,
   228  			0x1A, 0x1B, 0x10, 0x13,
   229  			0x19, 0x18, 0x17, 0x14,
   230  
   231  			0x03, 0x04, 0x09, 0x0A,
   232  			0x02, 0x05, 0x0E, 0x0D,
   233  			0x1D, 0x1C, 0x11, 0x12,
   234  			0x1E, 0x1F, 0x16, 0x15,
   235  
   236  			0x3C, 0x3B, 0x36, 0x35,
   237  			0x3D, 0x3A, 0x31, 0x32,
   238  			0x22, 0x23, 0x2E, 0x2D,
   239  			0x21, 0x20, 0x29, 0x2A,
   240  
   241  			0x3F, 0x38, 0x37, 0x34,
   242  			0x3E, 0x39, 0x30, 0x33,
   243  			0x25, 0x24, 0x2F, 0x2C,
   244  			0x26, 0x27, 0x28, 0x2B,
   245  		},
   246  	}
   247  
   248  	for order, expected := range cases {
   249  		t.Run(fmt.Sprintf("Case/%d", order), func(t *testing.T) {
   250  			testCurveCase(t, Hilbert3D{order: order}, order, expected)
   251  		})
   252  	}
   253  }
   254  
   255  func TestHilbert4D(t *testing.T) {
   256  	for ord := 1; ord <= 4; ord++ {
   257  		t.Run(fmt.Sprintf("Order %d", ord), func(t *testing.T) {
   258  			testCurve(t, Hilbert4D{order: ord})
   259  		})
   260  	}
   261  }
   262  
   263  func BenchmarkHilbert(b *testing.B) {
   264  	const O = 10
   265  	for N := 2; N <= 4; N++ {
   266  		b.Run(fmt.Sprintf("%dD/Pos", N), func(b *testing.B) {
   267  			for ord := 1; ord <= O; ord++ {
   268  				b.Run(fmt.Sprintf("Order %d", ord), func(b *testing.B) {
   269  					h := newCurve(ord, N)
   270  					v := make([]int, N)
   271  					for i := range v {
   272  						v[i] = rand.Intn(1 << ord)
   273  					}
   274  					u := make([]int, N)
   275  					for n := 0; n < b.N; n++ {
   276  						copy(u, v)
   277  						h.Pos(u)
   278  					}
   279  				})
   280  			}
   281  		})
   282  
   283  		b.Run(fmt.Sprintf("%dD/Coord", N), func(b *testing.B) {
   284  			for ord := 1; ord <= O; ord++ {
   285  				b.Run(fmt.Sprintf("Order %d", ord), func(b *testing.B) {
   286  					h := newCurve(ord, N)
   287  					d := rand.Intn(1 << (ord * N))
   288  					v := make([]int, N)
   289  					for n := 0; n < b.N; n++ {
   290  						h.Coord(v, d)
   291  					}
   292  				})
   293  			}
   294  		})
   295  	}
   296  }
   297  
   298  type curve interface {
   299  	Dims() []int
   300  	Len() int
   301  	Pos(v []int) int
   302  	Coord(dst []int, pos int) []int
   303  }
   304  
   305  func newCurve(order, dim int) curve {
   306  	switch dim {
   307  	case 2:
   308  		return Hilbert2D{order: order}
   309  	case 3:
   310  		return Hilbert3D{order: order}
   311  	case 4:
   312  		return Hilbert4D{order: order}
   313  	}
   314  	panic("invalid dimension")
   315  }
   316  
   317  // testCurve verifies that Pos and Coord (of C) are inverses of each other and
   318  // that the spatial coordinates V and U - corresponding to linear coordinates D
   319  // and D+1 - are exactly one unit (euclidean) distant from each other.
   320  func testCurve(t *testing.T, c curve) {
   321  	t.Helper()
   322  
   323  	// Stop if the error count reaches 10
   324  	var errc int
   325  	fail := func() {
   326  		if errc < 10 {
   327  			errc++
   328  			t.Fail()
   329  		} else {
   330  			t.FailNow()
   331  		}
   332  	}
   333  
   334  	// Map between linear and spatial coordinates, and verify that Pos and Coord
   335  	// are inverses of each other
   336  	m := map[int][]int{}
   337  	curveRange(c, func(v []int) {
   338  		d := c.Pos(slices.Clone(v))
   339  		u := c.Coord(nil, d)
   340  		if !reflect.DeepEqual(v, u) {
   341  			t.Logf("Space is not the inverse of Curve for d=%d %v", d, v)
   342  			fail()
   343  		}
   344  
   345  		m[d] = slices.Clone(v)
   346  	})
   347  
   348  	D := 1
   349  	for _, v := range c.Dims() {
   350  		D *= v
   351  	}
   352  
   353  	// For each possible pairs of linear coordinates D and D+1, verify that the
   354  	// corresponding spatial coordinates V and U are exactly one unit apart
   355  	// (euclidean distance)
   356  	for d := 0; d < D-1; d++ {
   357  		v, u := m[d], m[d+1]
   358  		if !adjacent(v, u) {
   359  			t.Logf("points %x and %x are not adjacent\n    %v -> %v", d, d+1, v, u)
   360  			fail()
   361  		}
   362  	}
   363  }
   364  
   365  // curveRange ranges over the n-dimensional coordinate space of the curve,
   366  // calling fn on each element of the space.
   367  func curveRange(c curve, fn func([]int)) {
   368  	size := c.Dims()
   369  	dimRange(len(size), size, make([]int, len(size)), fn)
   370  }
   371  
   372  // dimRange ranges over the coordinate space defined by size, calling fn on each
   373  // element of the space. Call dimRange with dim = len(size).
   374  func dimRange(dim int, size []int, v []int, fn func([]int)) {
   375  	if dim == 0 {
   376  		fn(v)
   377  		return
   378  	}
   379  
   380  	for i := 0; i < size[dim-1]; i++ {
   381  		v[dim-1] = i
   382  		dimRange(dim-1, size, v, fn)
   383  	}
   384  }
   385  
   386  // adjacent returns true if the euclidean distance between v and u is
   387  // exactly one. v and u must be the same length.
   388  //
   389  // In other words, given d(i) = abs(v[i] - u[i]), adjacent returns false if
   390  // d(i) > 1 for any i or if d(i) == 1 for more than a single i, or if d(i)
   391  // == 0 for all i.
   392  func adjacent(v, u []int) bool {
   393  	n := 0
   394  	for i := range v {
   395  		x := v[i] - u[i]
   396  		if x == 0 {
   397  			continue
   398  		}
   399  		if x < -1 || 1 < x {
   400  			return false
   401  		}
   402  		n++
   403  	}
   404  
   405  	return n == 1
   406  }
   407  
   408  // testCurveCase verifies that the curve produces the expected sequence of
   409  // values.
   410  func testCurveCase(t *testing.T, c curve, order int, expected []int) {
   411  	t.Helper()
   412  
   413  	dim := len(c.Dims())
   414  	actual := make([]int, len(expected))
   415  	for i := range expected {
   416  		v := coord(i, order, dim)
   417  		actual[i] = c.Pos(slices.Clone(v))
   418  	}
   419  	if !reflect.DeepEqual(expected, actual) {
   420  		t.Fatalf("unexpected result:\ngot:  %v\nwant: %v", actual, expected)
   421  	}
   422  
   423  	for i, d := range expected {
   424  		v := coord(i, order, dim)
   425  		if !reflect.DeepEqual(v, c.Coord(nil, d)) {
   426  			t.Fatalf("[%d] expected %v for d = %d", i, v, d)
   427  		}
   428  	}
   429  }
   430  
   431  // coord returns the nth spatial coordinates for a dim-dimensional space with
   432  // sides 2^ord in row-major order.
   433  func coord(n, ord, dim int) []int {
   434  	v := make([]int, dim)
   435  	for i := 0; i < dim; i++ {
   436  		v[i] = n % (1 << ord)
   437  		n /= (1 << ord)
   438  	}
   439  	return v
   440  }
   441  
   442  func noError(t testing.TB, err error) {
   443  	t.Helper()
   444  	if err != nil {
   445  		t.Fatal(err)
   446  	}
   447  }
   448  
   449  func errorIs(t testing.TB, err, target error) {
   450  	t.Helper()
   451  	if err == nil {
   452  		t.Fatal("Expected an error")
   453  	}
   454  	if !errors.Is(err, target) {
   455  		t.Fatalf("Expected %v to be %v", err, target)
   456  	}
   457  }