| | """
|
| | Hypothesis Property-Based Tests for BinaryHDV
|
| | ==============================================
|
| | Mathematical correctness tests using property-based testing.
|
| |
|
| | These tests validate the algebraic properties of BinaryHDV operations
|
| | using Hypothesis for automatic test case generation.
|
| | """
|
| |
|
| | import numpy as np
|
| | import pytest
|
| | from hypothesis import given, settings, HealthCheck, assume
|
| | import hypothesis.strategies as st
|
| |
|
| | from mnemocore.core.binary_hdv import BinaryHDV, majority_bundle
|
| |
|
| |
|
| |
|
| | TEST_DIMENSION = 512
|
| |
|
| |
|
| |
|
| | @st.composite
|
| | def binary_hdv_strategy(draw, dimension: int = TEST_DIMENSION):
|
| | """Generate a random BinaryHDV vector."""
|
| |
|
| | n_bytes = dimension // 8
|
| | byte_list = [draw(st.integers(min_value=0, max_value=255)) for _ in range(n_bytes)]
|
| | data = np.array(byte_list, dtype=np.uint8)
|
| | return BinaryHDV(data=data, dimension=dimension)
|
| |
|
| |
|
| | @st.composite
|
| | def binary_hdv_pair_strategy(draw, dimension: int = TEST_DIMENSION):
|
| | """Generate a pair of BinaryHDV vectors with the same dimension."""
|
| | v1 = draw(binary_hdv_strategy(dimension))
|
| | v2 = draw(binary_hdv_strategy(dimension))
|
| | return (v1, v2)
|
| |
|
| |
|
| | @st.composite
|
| | def binary_hdv_triple_strategy(draw, dimension: int = TEST_DIMENSION):
|
| | """Generate a triple of BinaryHDV vectors with the same dimension."""
|
| | v1 = draw(binary_hdv_strategy(dimension))
|
| | v2 = draw(binary_hdv_strategy(dimension))
|
| | v3 = draw(binary_hdv_strategy(dimension))
|
| | return (v1, v2, v3)
|
| |
|
| |
|
| | @st.composite
|
| | def shift_strategy(draw, dimension: int = TEST_DIMENSION):
|
| | """Generate a shift value for permute operations."""
|
| | return draw(st.integers(min_value=-dimension * 2, max_value=dimension * 2))
|
| |
|
| |
|
| | class TestBindCommutativity:
|
| | """Test commutativity property of bind(): a.bind(b) == b.bind(a)"""
|
| |
|
| | @given(vectors=binary_hdv_pair_strategy())
|
| | @settings(max_examples=100, deadline=None, suppress_health_check=[HealthCheck.too_slow])
|
| | def test_bind_commutativity(self, vectors):
|
| | """bind(a, b) == bind(b, a)"""
|
| | a, b = vectors
|
| | assert a.bind(b) == b.bind(a), "bind() must be commutative"
|
| |
|
| |
|
| | class TestBindUnbindInverse:
|
| | """Test inverse property: unbind(bind(a, b), b) == a"""
|
| |
|
| | @given(vectors=binary_hdv_pair_strategy())
|
| | @settings(max_examples=100, deadline=None, suppress_health_check=[HealthCheck.too_slow])
|
| | def test_bind_unbind_inverse(self, vectors):
|
| | """unbind(bind(a, b), b) == a"""
|
| | a, b = vectors
|
| |
|
| | bound = a.bind(b)
|
| | recovered = bound.unbind(b)
|
| | assert recovered == a, "unbind(bind(a, b), b) must equal a"
|
| |
|
| |
|
| | class TestPermuteSelfInverse:
|
| | """Test self-inverse property of permute(): permute(permute(a, k), -k) == a"""
|
| |
|
| | @given(vectors=binary_hdv_strategy(), shift=shift_strategy())
|
| | @settings(max_examples=100, deadline=None, suppress_health_check=[HealthCheck.too_slow])
|
| | def test_permute_self_inverse(self, vectors, shift):
|
| | """permute(permute(a, k), -k) == a"""
|
| | a = vectors
|
| | assume(shift != 0)
|
| | permuted = a.permute(shift)
|
| | recovered = permuted.permute(-shift)
|
| | assert recovered == a, f"permute(permute(a, {shift}), {-shift}) must equal a"
|
| |
|
| | @given(vectors=binary_hdv_strategy(), shift=shift_strategy())
|
| | @settings(max_examples=100, deadline=None, suppress_health_check=[HealthCheck.too_slow])
|
| | def test_permute_full_cycle(self, vectors, shift):
|
| | """permute(a, dimension) == a (full cycle returns original)"""
|
| | a = vectors
|
| | assume(shift != 0)
|
| |
|
| | normalized_shift = shift % TEST_DIMENSION
|
| |
|
| | permuted_by_dim = a.permute(TEST_DIMENSION)
|
| | assert permuted_by_dim == a, "permute(a, dimension) must equal a"
|
| |
|
| |
|
| | class TestHammingDistanceIdentity:
|
| | """Test Hamming distance identity: hamming(a, a) == 0"""
|
| |
|
| | @given(vector=binary_hdv_strategy())
|
| | @settings(max_examples=100, deadline=None, suppress_health_check=[HealthCheck.too_slow])
|
| | def test_hamming_distance_identity(self, vector):
|
| | """hamming(a, a) == 0"""
|
| | a = vector
|
| | assert a.hamming_distance(a) == 0, "hamming(a, a) must equal 0"
|
| |
|
| |
|
| | class TestHammingDistanceSymmetry:
|
| | """Test Hamming distance symmetry: hamming(a, b) == hamming(b, a)"""
|
| |
|
| | @given(vectors=binary_hdv_pair_strategy())
|
| | @settings(max_examples=100, deadline=None, suppress_health_check=[HealthCheck.too_slow])
|
| | def test_hamming_distance_symmetry(self, vectors):
|
| | """hamming(a, b) == hamming(b, a)"""
|
| | a, b = vectors
|
| | assert a.hamming_distance(b) == b.hamming_distance(a), \
|
| | "hamming(a, b) must equal hamming(b, a)"
|
| |
|
| |
|
| | class TestHammingDistanceNormalization:
|
| | """Test Hamming distance normalization: normalized_distance in [0, 1]"""
|
| |
|
| | @given(vectors=binary_hdv_pair_strategy())
|
| | @settings(max_examples=100, deadline=None, suppress_health_check=[HealthCheck.too_slow])
|
| | def test_normalized_distance_range(self, vectors):
|
| | """normalized_distance(a, b) in [0.0, 1.0]"""
|
| | a, b = vectors
|
| | nd = a.normalized_distance(b)
|
| | assert 0.0 <= nd <= 1.0, f"normalized_distance must be in [0, 1], got {nd}"
|
| |
|
| | @given(vectors=binary_hdv_pair_strategy())
|
| | @settings(max_examples=100, deadline=None, suppress_health_check=[HealthCheck.too_slow])
|
| | def test_similarity_range(self, vectors):
|
| | """similarity(a, b) in [0.0, 1.0]"""
|
| | a, b = vectors
|
| | sim = a.similarity(b)
|
| | assert 0.0 <= sim <= 1.0, f"similarity must be in [0, 1], got {sim}"
|
| |
|
| | @given(vectors=binary_hdv_pair_strategy())
|
| | @settings(max_examples=100, deadline=None, suppress_health_check=[HealthCheck.too_slow])
|
| | def test_normalized_distance_consistency(self, vectors):
|
| | """normalized_distance(a, b) == hamming_distance(a, b) / dimension"""
|
| | a, b = vectors
|
| | expected = a.hamming_distance(b) / a.dimension
|
| | actual = a.normalized_distance(b)
|
| | assert actual == expected, \
|
| | f"normalized_distance must equal hamming_distance / dimension"
|
| |
|
| |
|
| | class TestDeterminism:
|
| | """Test determinism: same input always produces same output"""
|
| |
|
| | @given(seed=st.text(min_size=1, max_size=100))
|
| | @settings(max_examples=50, deadline=None, suppress_health_check=[HealthCheck.too_slow])
|
| | def test_from_seed_determinism(self, seed):
|
| | """from_seed(seed) always produces the same vector"""
|
| | v1 = BinaryHDV.from_seed(seed, TEST_DIMENSION)
|
| | v2 = BinaryHDV.from_seed(seed, TEST_DIMENSION)
|
| | assert v1 == v2, f"from_seed('{seed}') must be deterministic"
|
| |
|
| | @given(vector=binary_hdv_strategy())
|
| | @settings(max_examples=100, deadline=None, suppress_health_check=[HealthCheck.too_slow])
|
| | def test_xor_bind_determinism(self, vector):
|
| | """bind(a, b) always produces the same result for same inputs"""
|
| | a = vector
|
| |
|
| | b = BinaryHDV.random(TEST_DIMENSION)
|
| | result1 = a.bind(b)
|
| | result2 = a.bind(b)
|
| | assert result1 == result2, "bind() must be deterministic"
|
| |
|
| | @given(vector=binary_hdv_strategy(), shift=shift_strategy())
|
| | @settings(max_examples=100, deadline=None, suppress_health_check=[HealthCheck.too_slow])
|
| | def test_permute_determinism(self, vector, shift):
|
| | """permute(a, shift) always produces the same result for same inputs"""
|
| | a = vector
|
| | result1 = a.permute(shift)
|
| | result2 = a.permute(shift)
|
| | assert result1 == result2, "permute() must be deterministic"
|
| |
|
| |
|
| | class TestAdditionalAlgebraicProperties:
|
| | """Additional algebraic property tests"""
|
| |
|
| | @given(vectors=binary_hdv_triple_strategy())
|
| | @settings(max_examples=100, deadline=None, suppress_health_check=[HealthCheck.too_slow])
|
| | def test_bind_associativity(self, vectors):
|
| | """(a.bind(b)).bind(c) == a.bind(b.bind(c))"""
|
| | a, b, c = vectors
|
| | lhs = a.bind(b).bind(c)
|
| | rhs = a.bind(b.bind(c))
|
| | assert lhs == rhs, "bind() must be associative"
|
| |
|
| | @given(vector=binary_hdv_strategy())
|
| | @settings(max_examples=100, deadline=None, suppress_health_check=[HealthCheck.too_slow])
|
| | def test_bind_self_inverse(self, vector):
|
| | """a.bind(a) == zeros"""
|
| | a = vector
|
| | result = a.bind(a)
|
| | zeros = BinaryHDV.zeros(TEST_DIMENSION)
|
| | assert result == zeros, "a.bind(a) must equal zero vector"
|
| |
|
| | @given(vectors=binary_hdv_triple_strategy())
|
| | @settings(max_examples=100, deadline=None, suppress_health_check=[HealthCheck.too_slow])
|
| | def test_hamming_triangle_inequality(self, vectors):
|
| | """hamming(a, c) <= hamming(a, b) + hamming(b, c)"""
|
| | a, b, c = vectors
|
| | assert a.hamming_distance(c) <= a.hamming_distance(b) + b.hamming_distance(c), \
|
| | "Hamming distance must satisfy triangle inequality"
|
| |
|
| | @given(vectors=binary_hdv_triple_strategy())
|
| | @settings(max_examples=100, deadline=None, suppress_health_check=[HealthCheck.too_slow])
|
| | def test_bind_preserves_distance(self, vectors):
|
| | """hamming(a.bind(c), b.bind(c)) == hamming(a, b)"""
|
| | a, b, c = vectors
|
| | dist_ab = a.hamming_distance(b)
|
| | dist_ac_bc = a.bind(c).hamming_distance(b.bind(c))
|
| | assert dist_ab == dist_ac_bc, \
|
| | f"bind must preserve distance: {dist_ab} != {dist_ac_bc}"
|
| |
|
| | @given(vector=binary_hdv_strategy())
|
| | @settings(max_examples=100, deadline=None, suppress_health_check=[HealthCheck.too_slow])
|
| | def test_invert_is_max_distance(self, vector):
|
| | """hamming(a, a.invert()) == dimension"""
|
| | a = vector
|
| | assert a.hamming_distance(a.invert()) == TEST_DIMENSION, \
|
| | "hamming(a, ~a) must equal dimension"
|
| |
|
| | @given(vector=binary_hdv_strategy())
|
| | @settings(max_examples=100, deadline=None, suppress_health_check=[HealthCheck.too_slow])
|
| | def test_invert_is_self_inverse(self, vector):
|
| | """a.invert().invert() == a"""
|
| | a = vector
|
| | recovered = a.invert().invert()
|
| | assert recovered == a, "double invert must return original"
|
| |
|