mirror of
https://github.com/SikongJueluo/Mini-Nav.git
synced 2026-07-13 04:25:32 +08:00
Introduce hash_codec module providing bidirectional encoding/decoding: - bits_tensor_to_hash_bytes / hash_bytes_to_bits_array - bits_tensor_to_cam_row - hash_bytes_to_cam_row / cam_row_to_hash_bytes
183 lines
5.7 KiB
Python
183 lines
5.7 KiB
Python
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pytest
|
|
import torch
|
|
|
|
|
|
MINI_NAV_DIR = Path(__file__).resolve().parents[1] / "mini-nav"
|
|
sys.path.insert(0, str(MINI_NAV_DIR))
|
|
|
|
from scenegraph.hash_codec import ( # noqa: E402
|
|
bits_tensor_to_cam_row,
|
|
bits_tensor_to_hash_bytes,
|
|
cam_row_to_hash_bytes,
|
|
hash_bytes_to_bits_array,
|
|
hash_bytes_to_cam_row,
|
|
)
|
|
|
|
|
|
WIDTH = 512
|
|
|
|
|
|
def _xnor_score(query_row: int, stored_row: int, *, width: int = WIDTH) -> int:
|
|
mask = (1 << width) - 1
|
|
return int((~(query_row ^ stored_row) & mask).bit_count())
|
|
|
|
|
|
def _hamming_distance(left: np.ndarray, right: np.ndarray) -> int:
|
|
return int((left != right).sum())
|
|
|
|
|
|
def test_all_zero_hash_roundtrips_through_bytes_and_cam_row():
|
|
bits = torch.zeros(WIDTH, dtype=torch.int32)
|
|
|
|
hash_bytes = bits_tensor_to_hash_bytes(bits)
|
|
cam_row = hash_bytes_to_cam_row(hash_bytes)
|
|
roundtrip = cam_row_to_hash_bytes(cam_row)
|
|
|
|
assert len(hash_bytes) == WIDTH // 8
|
|
assert hash_bytes == bytes(WIDTH // 8)
|
|
assert cam_row == 0
|
|
assert roundtrip == hash_bytes
|
|
|
|
|
|
def test_all_one_hash_roundtrips_through_bytes_and_cam_row():
|
|
bits = torch.ones(WIDTH, dtype=torch.int32)
|
|
|
|
hash_bytes = bits_tensor_to_hash_bytes(bits)
|
|
cam_row = hash_bytes_to_cam_row(hash_bytes)
|
|
roundtrip = cam_row_to_hash_bytes(cam_row)
|
|
|
|
assert hash_bytes == b"\xff" * (WIDTH // 8)
|
|
assert cam_row == (1 << WIDTH) - 1
|
|
assert roundtrip == hash_bytes
|
|
|
|
|
|
def test_first_bit_uses_packbits_high_bit_ordering():
|
|
bits = torch.zeros(WIDTH, dtype=torch.int32)
|
|
bits[0] = 1
|
|
|
|
hash_bytes = bits_tensor_to_hash_bytes(bits)
|
|
cam_row = hash_bytes_to_cam_row(hash_bytes)
|
|
|
|
assert hash_bytes[0] == 0b10000000
|
|
assert cam_row == 1 << (WIDTH - 1)
|
|
|
|
|
|
def test_last_bit_maps_to_least_significant_cam_row_bit():
|
|
bits = torch.zeros(WIDTH, dtype=torch.int32)
|
|
bits[WIDTH - 1] = 1
|
|
|
|
hash_bytes = bits_tensor_to_hash_bytes(bits)
|
|
cam_row = hash_bytes_to_cam_row(hash_bytes)
|
|
|
|
assert hash_bytes[-1] == 0b00000001
|
|
assert cam_row == 1
|
|
|
|
|
|
def test_hash_bytes_unpack_to_bits_array_with_packbits_ordering():
|
|
hash_bytes = bytes([0b10100000]) + bytes((WIDTH // 8) - 1)
|
|
|
|
bits = hash_bytes_to_bits_array(hash_bytes)
|
|
|
|
assert bits.shape == (WIDTH,)
|
|
assert bits.dtype == np.uint8
|
|
assert bits[:4].tolist() == [1, 0, 1, 0]
|
|
assert bits[4:].sum() == 0
|
|
|
|
|
|
def test_bits_tensor_to_cam_row_matches_bytes_conversion():
|
|
bits = torch.zeros(WIDTH, dtype=torch.int32)
|
|
bits[0] = 1
|
|
bits[7] = 1
|
|
bits[511] = 1
|
|
|
|
hash_bytes = bits_tensor_to_hash_bytes(bits)
|
|
|
|
assert bits_tensor_to_cam_row(bits) == hash_bytes_to_cam_row(hash_bytes)
|
|
|
|
|
|
def test_positive_threshold_accepts_bool_and_signed_hash_encodings():
|
|
bool_bits = torch.zeros(WIDTH, dtype=torch.bool)
|
|
bool_bits[0] = True
|
|
signed_bits = torch.full((WIDTH,), -1, dtype=torch.int32)
|
|
signed_bits[0] = 1
|
|
|
|
assert bits_tensor_to_hash_bytes(bool_bits) == bits_tensor_to_hash_bytes(signed_bits)
|
|
|
|
|
|
def test_xnor_score_matches_width_minus_hamming_distance():
|
|
zeros = torch.zeros(WIDTH, dtype=torch.int32)
|
|
ones = torch.ones(WIDTH, dtype=torch.int32)
|
|
one_bit_diff = torch.zeros(WIDTH, dtype=torch.int32)
|
|
one_bit_diff[0] = 1
|
|
|
|
zero_bytes = bits_tensor_to_hash_bytes(zeros)
|
|
one_bytes = bits_tensor_to_hash_bytes(ones)
|
|
one_diff_bytes = bits_tensor_to_hash_bytes(one_bit_diff)
|
|
|
|
zero_row = hash_bytes_to_cam_row(zero_bytes)
|
|
one_row = hash_bytes_to_cam_row(one_bytes)
|
|
one_diff_row = hash_bytes_to_cam_row(one_diff_bytes)
|
|
|
|
zero_bits = hash_bytes_to_bits_array(zero_bytes)
|
|
one_bits = hash_bytes_to_bits_array(one_bytes)
|
|
one_diff_bits = hash_bytes_to_bits_array(one_diff_bytes)
|
|
|
|
assert _hamming_distance(zero_bits, zero_bits) == 0
|
|
assert _xnor_score(zero_row, zero_row) == WIDTH
|
|
|
|
assert _hamming_distance(zero_bits, one_diff_bits) == 1
|
|
assert _xnor_score(zero_row, one_diff_row) == WIDTH - 1
|
|
|
|
assert _hamming_distance(zero_bits, one_bits) == WIDTH
|
|
assert _xnor_score(zero_row, one_row) == 0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"bits",
|
|
[
|
|
torch.zeros(WIDTH - 1, dtype=torch.int32),
|
|
torch.zeros(WIDTH + 1, dtype=torch.int32),
|
|
],
|
|
)
|
|
def test_bits_tensor_to_hash_bytes_rejects_wrong_width(bits: torch.Tensor):
|
|
with pytest.raises(ValueError, match="exactly 512 values"):
|
|
bits_tensor_to_hash_bytes(bits)
|
|
|
|
|
|
def test_hash_bytes_to_bits_array_rejects_wrong_byte_length():
|
|
with pytest.raises(ValueError, match="exactly 64 bytes"):
|
|
hash_bytes_to_bits_array(bytes(63))
|
|
|
|
|
|
@pytest.mark.parametrize("cam_row", [-1, 1 << WIDTH])
|
|
def test_cam_row_to_hash_bytes_rejects_out_of_range_rows(cam_row: int):
|
|
with pytest.raises(ValueError, match="range"):
|
|
cam_row_to_hash_bytes(cam_row)
|
|
|
|
|
|
def test_width_must_be_divisible_by_eight():
|
|
with pytest.raises(ValueError, match="divisible by 8"):
|
|
bits_tensor_to_hash_bytes(torch.zeros(7, dtype=torch.int32), width=7)
|
|
|
|
|
|
def test_scenegraph_package_exports_hash_codec_helpers():
|
|
from scenegraph import ( # noqa: PLC0415
|
|
bits_tensor_to_cam_row as exported_bits_tensor_to_cam_row,
|
|
bits_tensor_to_hash_bytes as exported_bits_tensor_to_hash_bytes,
|
|
cam_row_to_hash_bytes as exported_cam_row_to_hash_bytes,
|
|
hash_bytes_to_bits_array as exported_hash_bytes_to_bits_array,
|
|
hash_bytes_to_cam_row as exported_hash_bytes_to_cam_row,
|
|
)
|
|
|
|
assert exported_bits_tensor_to_cam_row is bits_tensor_to_cam_row
|
|
assert exported_bits_tensor_to_hash_bytes is bits_tensor_to_hash_bytes
|
|
assert exported_cam_row_to_hash_bytes is cam_row_to_hash_bytes
|
|
assert exported_hash_bytes_to_bits_array is hash_bytes_to_bits_array
|
|
assert exported_hash_bytes_to_cam_row is hash_bytes_to_cam_row
|