Skip to content

Instantly share code, notes, and snippets.

@JossWhittle
Created March 6, 2018 16:38
Show Gist options
  • Select an option

  • Save JossWhittle/12afba3c8e1c5d69c7736d6e4292495f to your computer and use it in GitHub Desktop.

Select an option

Save JossWhittle/12afba3c8e1c5d69c7736d6e4292495f to your computer and use it in GitHub Desktop.
"""
This is a module to convert between one dimensional distance along a
`Hilbert curve`_, :math:`h`, and N-dimensional coordinates,
:math:`(x_0, x_1, ... x_N)`. The two important parameters are :math:`N`
(the number of dimensions, must be > 0) and :math:`p` (the number of
iterations used in constructing the Hilbert curve, must be > 0).
We consider an N-dimensional `hypercube`_ of side length :math:`2^p`.
This hypercube contains :math:`2^{N p}` unit hypercubes (:math:`2^p` along
each dimension). The number of unit hypercubes determine the possible
discrete distances along the Hilbert curve (indexed from :math:`0` to
:math:`2^{N p} - 1`). The image below illustrates the situation for
:math:`N=2` and :math:`p=3`.
.. figure:: nD=2_p=3.png
This is the third iteration (:math:`p=3`) of the Hilbert curve in two
(:math:`N=2`) dimensions. Distances, :math:`h`, along the curve are
labeled from 0 to 63 (i.e. from 0 to :math:`2^{N p}-1`). The provided
functions translate between N-dimensional coordinates and the one
dimensional distance. For example, between (:math:`x_0=4, x_1=6`) and
:math:`h=36`.
Reference
=========
This module is based on the C code provided in the 2004 article
"Programming the Hilbert Curve" by John Skilling,
- http://adsabs.harvard.edu/abs/2004AIPC..707..381S
I was also helped by the discussion in the following stackoverflow post,
- `mapping-n-dimensional-value-to-a-point-on-hilbert-curve`_
which points out a typo in the source code of the paper. The Skilling code
provides two functions ``TransposetoAxes`` and ``AxestoTranspose``. In this
case, Transpose refers to a specific packing of the integer that represents
distance along the Hilbert curve (see below for details) and
Axes refer to the N-dimensional coordinates. Below is an excerpt of the docs
from that code that appears in the paper by Skilling, ::
//+++++++++++++++++++++++++++ PUBLIC-DOMAIN SOFTWARE ++++++++++++++++++++++++++
// Functions: TransposetoAxes AxestoTranspose
// Purpose: Transform in-place between Hilbert transpose and geometrical axes
// Example: b=5 bits for each of n=3 coordinates.
// 15-bit Hilbert integer = A B C D E F G H I J K L M N O is stored
// as its Transpose
// X[0] = A D G J M X[2]|
// X[1] = B E H K N <-------> | /X[1]
// X[2] = C F I L O axes |/
// high low 0------ X[0]
// Axes are stored conveniently as b-bit integers.
// Author: John Skilling 20 Apr 2001 to 11 Oct 2003
.. _Hilbert curve: https://en.wikipedia.org/wiki/Hilbert_curve
.. _hypercube: https://en.wikipedia.org/wiki/Hypercube
.. _mapping-n-dimensional-value-to-a-point-on-hilbert-curve: http://stackoverflow.com/questions/499166/mapping-n-dimensional-value-to-a-point-on-hilbert-curve/10384110#10384110
"""
def _binary_repr(num, width):
"""Return a binary string representation of `num` zero padded to `width`
bits."""
return format(num, 'b').zfill(width)
class HilbertCurve:
def __init__(self, p, n):
"""Initialize a hilbert curve with,
Args:
p (int): iterations to use in the hilbert curve
n (int): number of dimensions
"""
self.p = p
self.n = n
def _hilbert_integer_to_transpose(self, h):
"""Store a hilbert integer (`h`) as its transpose (`x`).
Args:
h (int): integer distance along hilbert curve
Returns:
x (list): transpose of h
(n components with values between 0 and 2**p-1)
"""
h_bit_str = _binary_repr(h, self.p*self.n)
x = [int(h_bit_str[i::self.n], 2) for i in range(self.n)]
return x
def _transpose_to_hilbert_integer(self, x):
"""Restore a hilbert integer (`h`) from its transpose (`x`).
Args:
x (list): transpose of h
(n components with values between 0 and 2**p-1)
Returns:
h (int): integer distance along hilbert curve
"""
x_bit_str = [_binary_repr(x[i], self.p) for i in range(self.n)]
h = int(''.join([y[i] for i in range(self.p) for y in x_bit_str]), 2)
return h
def coordinates_from_distance(self, h):
"""Return the coordinates for a given hilbert distance.
Args:
h (int): integer distance along hilbert curve
Returns:
x (list): transpose of h
(n components with values between 0 and 2**p-1)
"""
max_h = 2**(self.p * self.n) - 1
if h > max_h:
raise ValueError('h={} is greater than 2**(p*N)-1={}'.format(h, max_h))
x = self._hilbert_integer_to_transpose(h)
Z = 2 << (self.p-1)
# Gray decode by H ^ (H/2)
t = x[self.n-1] >> 1
for i in range(self.n-1, 0, -1):
x[i] ^= x[i-1]
x[0] ^= t
# Undo excess work
Q = 2
while Q != Z:
P = Q - 1
for i in range(self.n-1, -1, -1):
if x[i] & Q:
# invert
x[0] ^= P
else:
# exchange
t = (x[0] ^ x[i]) & P
x[0] ^= t
x[i] ^= t
Q <<= 1
# done
return x
def distance_from_coordinates(self, x):
"""Return the hilbert distance for a given set of coordinates.
Args:
x (list): transpose of h
(n components with values between 0 and 2**p-1)
Returns:
h (int): integer distance along hilbert curve
"""
if len(x) != self.n:
raise ValueError('x={} must have N={} dimensions'.format(x, self.n))
max_x = 2**self.p - 1
if any(elx > max_x for elx in x):
raise ValueError(
'invalid coordinate input x={}. one or more dimensions have a '
'value greater than 2**p-1={}'.format(x, max_x))
M = 1 << (self.p - 1)
# Inverse undo excess work
Q = M
while Q > 1:
P = Q - 1
for i in range(self.n):
if x[i] & Q:
x[0] ^= P
else:
t = (x[0] ^ x[i]) & P
x[0] ^= t
x[i] ^= t
Q >>= 1
# Gray encode
for i in range(1, self.n):
x[i] ^= x[i-1]
t = 0
Q = M
while Q > 1:
if x[self.n-1] & Q:
t ^= Q - 1
Q >>= 1
for i in range(self.n):
x[i] ^= t
h = self._transpose_to_hilbert_integer(x)
return h
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment