Created
March 6, 2018 16:38
-
-
Save JossWhittle/12afba3c8e1c5d69c7736d6e4292495f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| 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