This algorithm encodes a 64 bit integer.
As input I have a 64 bit integer a
, for example:
(a is a single 64 bit number,which I split in 8 bytes for readability) a=11110110,10101110,00010000,01100110,11101011,11000001,11001011,01100011
To calculate b=f(a)
, I begin with the rightmost digit (the less significant digit) and write '0'. Then, I work my way leftward. Each time I encounter a '1' in a
, I switch between writing '0' and '1'. For example, if a=1011
, then b=0010
.
Here is an example of 64 bit a
and b=f(a)
a=11110110,10101110,00010000,01100110,11101011,11000001,11001011,01100011
b=10100100,11001011,11100000,01000100,10110010,10000001,01110010,01000010
This is dumb code that calculates it for a list of bytes:
# Slow function to memoize f(numbers_to_memoize) up to 8 bits
# Returns the encoded list
def encode_reversed_gray_code(numbers: list) -> list:
# Convert to binary
numbers_bin = np.vectorize(np.binary_repr)(numbers, width=bit_length)
# Convert each string to a list of digits
numbers_bin_list = [list(x) for x in numbers_bin]
# encode reversed gray code
gray_code = []
for _list in numbers_bin_list:
reversed_number = _list[::-1]
next_digit_is_1 = False
encoded_list_reversed = ["0"]
for x in reversed_number:
if x == "1":
next_digit_is_1 = not next_digit_is_1
encoded_list_reversed.append(["0", "1"][next_digit_is_1])
gray_code.append(int("".join(encoded_list_reversed[-2::-1]), 2))
# print("".join(new_list[::-1]))
return np.asarray(gray_code).astype(np.uint8)
That code is slow, but runs only once. I use it to accelerate the calculation for 64 bit.
I need a lot more speed, so I thought to memoize the result in a table, but the table would have 2⁶⁴ uint64, which is too much memory.
So, I thought to memoize only up to 8 bits, which only requires 256 bytes of memory. And build the 64 bit code as a paste of 8 bytes encoded. This will allow me to use the memoization tables for 8-bit integers to encode larger integers.
With the above code, I made an 8-bit table named gray_code_starting_in_0
The 8-bit table is a convenient approach, because most calculations will involve much smaller numbers than 64 bit. So, the small table will likely be useful on its own, requiring only one or two lookups, and I guess will fit easily into the cpu cache, meanwhile an 16 bit will probably not.
The caveat is that although the encoding of the entire 64-bit uint starts with 0
, the individual bytes may not. A new byte encoding does not necessarily start with 0
, but with the most significant bit of the last byte XORed. If the last byte was byte_a
, and its encoding was code(byte_a)
, the next byte starts with (byte_a ^ code(byte_a)) >> 7
.
So I made a second table memoizing the same encoding, but starting with "1" instead of "0":
gray_code_starting_in_0 = not gray_code_starting_in_0
this is the complete code:
import numpy as np
bit_length = 8
numbers_to_memoize = np.arange(0, 2**bit_length).astype(np.uint8)
# Slow function to memoize f(numbers_to_memoize) up to 8 bits
# Returns the encoded list
def encode_reversed_gray_code(numbers: list) -> (list, list):
# Convert to binary
numbers_bin = np.vectorize(np.binary_repr)(numbers, width=bit_length)
# Convert each string to a list of digits
numbers_bin_list = [list(x) for x in numbers_bin]
# encode reversed gray code
gray_code = []
for _list in numbers_bin_list:
reversed_number = _list[::-1]
next_digit_is_1 = False
encoded_list_reversed = ["0"]
for x in reversed_number:
if x == "1":
next_digit_is_1 = not next_digit_is_1
encoded_list_reversed.append(["0", "1"][next_digit_is_1])
gray_code.append(int("".join(encoded_list_reversed[-2::-1]), 2))
# print("".join(new_list[::-1]))
return np.asarray(gray_code).astype(np.uint8)
gray_code_starting_in_0 = encode_reversed_gray_code(numbers_to_memoize)
# the bitwise_not, calculates the code as if it started with 1 instead of 0
gray_code_starting_in_1 = np.vectorize(np.bitwise_not)(gray_code_starting_in_0).astype(
np.uint8
)
# Dummy variable to be overwritten, to avoid creating a new one each time a code is caculated
chunks_encoded = np.asarray([0] * 8, dtype=np.uint8)
# Weights to reconvert the array of bytes into a 64 bit uint
chunk_weights = 2 ** np.arange(0, 8**2, 8, dtype=np.uint64)
FF = 0xFF
# code to split a 64-bit integer into bytes
import struct
pack = struct.pack
unpack = struct.unpack
# Usage:
# 8_byte_chunks_of_n = unpack("8B", pack("Q", n))
from math import ceil
def fast_64_bit_encoding(n: int) -> int:
# split n in byte sized chunks
n = int(n)
# split 64 bit integer into 8 bit chunks
number_of_chunks = ceil(n.bit_length() / 8)
chunks = unpack("8B", pack("Q", n))[:number_of_chunks]
next_starting_bit_is_1 = False
for index, chunk in enumerate(chunks):
# print(",".join(np.vectorize(np.binary_repr)(chunks[::-1], width=bit_length)))
# print(
# ",".join(
# np.vectorize(np.binary_repr)(chunks_encoded[::-1], width=bit_length)
# )
# )
# print()
if next_starting_bit_is_1:
chunks_encoded[index] = gray_code_starting_in_1[chunk]
else:
chunks_encoded[index] = gray_code_starting_in_0[chunk]
next_starting_bit_is_1 = (chunk ^ chunks_encoded[index]) >> 7 == 1
# # assert that next starting bit is correct
# assert (
# ((chunk ^ chunks_encoded[index]) & int("10000000", 2)) > 0
# ) == next_starting_bit_is_1, "".join(
# [
# f""" chunk {np.binary_repr(chunk, width=bit_length)}\n""",
# f"""code: {np.binary_repr(chunks_encoded[index], width=bit_length)}\n""",
# f"""next bit: { next_starting_bit_is_1}""",
# ]
# )
return np.dot(chunks_encoded, chunk_weights)
# Random uint64 to test the encoding
a = int("1" + "".join(np.random.choice(["0", "1"], size=63)), 2)
answer = f"{np.binary_repr(fast_64_bit_encoding(a))}"
# split "a" and "answer" in bytes
bin_a = bin(a)[2:]
a_split = [bin_a[i : i + 8] for i in range(0, len(bin_a), 8)]
answer = [answer[i : i + 8] for i in range(0, len(answer), 8)]
print("a=" + ",".join(a_split))
print("b=" + ",".join(answer))
Note: the OEIS sequence A006068 calculates the same thing, but from left to right. That webpage has slow code (but simpler) for that calculation:
def oeis_A006068(n: np.uint64):
s = 1
while True:
ns = n >> s
if ns == 0:
break
n = n ^ ns
s <<= 1
return n
oeis
shouldn't be slow, are you sure it is? Note that it does not shift by 1 every iteration, it shifts by 1, 2, 4, 8 etc and so uses only log(bits) iterations. E: and I, or you or anyone else, could make a right-to-left version of it as well \$\endgroup\$