|
1 | 1 | """Tests for the hypercube mathematical operations.""" |
2 | 2 |
|
| 3 | +import itertools |
3 | 4 | import math |
4 | 5 | from functools import lru_cache |
5 | | -from typing import List |
| 6 | +from typing import Any, Callable, List, Tuple |
6 | 7 |
|
7 | 8 | import pytest |
| 9 | +from hypothesis import assume, given, settings |
| 10 | +from hypothesis import strategies as st |
8 | 11 |
|
9 | 12 | from lean_spec.subspecs.xmss.hypercube import ( |
10 | 13 | MAX_DIMENSION, |
@@ -212,3 +215,146 @@ def test_big_map() -> None: |
212 | 215 | # Assert that both steps of the roundtrip were successful. |
213 | 216 | assert x == x_reconstructed |
214 | 217 | assert vertex_a == vertex_b |
| 218 | + |
| 219 | + |
| 220 | +# --------------------------- |
| 221 | +# Hypothesis property tests |
| 222 | +# --------------------------- |
| 223 | + |
| 224 | +DrawFn = Callable[[Any], Any] |
| 225 | + |
| 226 | + |
| 227 | +# Strategy for small hypercubes where we can feasibly enumerate |
| 228 | +@st.composite |
| 229 | +def small_hypercube(draw: DrawFn) -> Tuple[int, int]: |
| 230 | + w = draw(st.integers(min_value=2, max_value=6)) |
| 231 | + v = draw(st.integers(min_value=1, max_value=6)) |
| 232 | + assume(w**v <= 6000) # keep enumeration feasible |
| 233 | + return w, v |
| 234 | + |
| 235 | + |
| 236 | +@st.composite |
| 237 | +def small_hypercube_with_layer_and_index(draw: DrawFn) -> Tuple[int, int, int, int]: |
| 238 | + w = draw(st.integers(min_value=2, max_value=6)) |
| 239 | + v = draw(st.integers(min_value=1, max_value=6)) |
| 240 | + assume(w**v <= 6000) |
| 241 | + info = prepare_layer_info(w)[v] |
| 242 | + d = draw(st.integers(min_value=0, max_value=len(info.sizes) - 1)) |
| 243 | + size = info.sizes[d] |
| 244 | + assume(size > 0) |
| 245 | + x = draw(st.integers(min_value=0, max_value=size - 1)) |
| 246 | + return w, v, d, x |
| 247 | + |
| 248 | + |
| 249 | +# Strategies for two distinct vertices in a layer |
| 250 | +@st.composite |
| 251 | +def two_distinct_index(draw: DrawFn) -> Tuple[int, int, int, int, int]: |
| 252 | + w = draw(st.integers(2, 6)) |
| 253 | + v = draw(st.integers(1, 6)) |
| 254 | + assume(w**v <= 6000) |
| 255 | + info = prepare_layer_info(w)[v] |
| 256 | + # pick layer with size >= 2 |
| 257 | + candidates = [d for d, s in enumerate(info.sizes) if s >= 2] |
| 258 | + assume(candidates) |
| 259 | + d = draw(st.sampled_from(candidates)) |
| 260 | + s = info.sizes[d] |
| 261 | + x1 = draw(st.integers(0, s - 1)) |
| 262 | + x2 = draw(st.integers(0, s - 1)) |
| 263 | + assume(x2 != x1) |
| 264 | + return w, v, d, x1, x2 |
| 265 | + |
| 266 | + |
| 267 | +@given(small_hypercube()) |
| 268 | +@settings(max_examples=120) |
| 269 | +def test_property_layer_sizes_conservation_monotonicity(wv: Tuple[int, int]) -> None: |
| 270 | + """All vertices should be accounted for and prefix sums monotone.""" |
| 271 | + w, v = wv |
| 272 | + info = prepare_layer_info(w)[v] |
| 273 | + # conservation |
| 274 | + assert info.prefix_sums[-1] == w**v |
| 275 | + assert sum(info.sizes) == w**v |
| 276 | + # non-negative sizes |
| 277 | + assert all(s >= 0 for s in info.sizes) |
| 278 | + # monotone prefix sums |
| 279 | + assert all( |
| 280 | + info.prefix_sums[i] <= info.prefix_sums[i + 1] for i in range(len(info.prefix_sums) - 1) |
| 281 | + ) |
| 282 | + |
| 283 | + |
| 284 | +@given(small_hypercube_with_layer_and_index()) |
| 285 | +@settings(max_examples=200) |
| 286 | +def test_property_map_vertex_roundtrip(params: Tuple[int, int, int, int]) -> None: |
| 287 | + """Tests that map_to_vertex returns valid coords in layer d and |
| 288 | + map_to_integer inverts it correctly.""" |
| 289 | + w, v, d, x = params |
| 290 | + vertex = map_to_vertex(w, v, d, x) |
| 291 | + assert isinstance(vertex, list) and len(vertex) == v |
| 292 | + assert all(0 <= ai < w for ai in vertex) |
| 293 | + # membership in layer |
| 294 | + coord_sum = sum(vertex) |
| 295 | + assert (w - 1) * v - coord_sum == d |
| 296 | + # roundtrip via existing map_to_integer |
| 297 | + x_reconstructed = map_to_integer(w, v, d, vertex) |
| 298 | + assert x_reconstructed == x |
| 299 | + |
| 300 | + |
| 301 | +@given(small_hypercube()) |
| 302 | +@settings(max_examples=40, deadline=None) |
| 303 | +def test_property_bijectivity_enum(wv: Tuple[int, int]) -> None: |
| 304 | + """For tiny hypercubes, map_to_vertex enumerates exactly |
| 305 | + w**v distinct vertices.""" |
| 306 | + w, v = wv |
| 307 | + assume(w**v <= 2000) # extra safety |
| 308 | + info = prepare_layer_info(w)[v] |
| 309 | + seen = set() |
| 310 | + for d, size in enumerate(info.sizes): |
| 311 | + for x in range(size): |
| 312 | + a = tuple(map_to_vertex(w, v, d, x)) |
| 313 | + assert len(a) == v |
| 314 | + seen.add(a) |
| 315 | + assert len(seen) == w**v |
| 316 | + |
| 317 | + |
| 318 | +@given(small_hypercube()) |
| 319 | +@settings(max_examples=80) |
| 320 | +def test_property_map_oob(wv: Tuple[int, int]) -> None: |
| 321 | + """map_to_vertex should raise ValueError when x is equal |
| 322 | + to the layer size (out-of-range).""" |
| 323 | + w, v = wv |
| 324 | + info = prepare_layer_info(w)[v] |
| 325 | + for d, size in enumerate(info.sizes): |
| 326 | + if size > 0: |
| 327 | + with pytest.raises(ValueError): |
| 328 | + map_to_vertex(w, v, d, size) |
| 329 | + break |
| 330 | + |
| 331 | + |
| 332 | +@given(small_hypercube()) |
| 333 | +@settings(max_examples=120, deadline=None) |
| 334 | +def test_property_find_layer_prefix(wv: Tuple[int, int]) -> None: |
| 335 | + """hypercube_find_layer returns (d, r) consistent with |
| 336 | + prefix sums and global index.""" |
| 337 | + w, v = wv |
| 338 | + info = prepare_layer_info(w)[v] |
| 339 | + total = w**v |
| 340 | + candidates = {0, 1, total - 1, total // 2} |
| 341 | + for x in candidates: |
| 342 | + if not (0 <= x < total): |
| 343 | + continue |
| 344 | + d, r = hypercube_find_layer(w, v, x) |
| 345 | + assert 0 <= d < len(info.sizes) |
| 346 | + assert 0 <= r < info.sizes[d] |
| 347 | + prev = info.prefix_sums[d - 1] if d > 0 else 0 |
| 348 | + assert prev + r == x |
| 349 | + |
| 350 | + |
| 351 | +@given(two_distinct_index()) |
| 352 | +@settings(max_examples=200) |
| 353 | +def test_property_layer_injectivity(params: Tuple[int, int, int, int, int]) -> None: |
| 354 | + """Ensure injectivity inside a layer for map_to_vertex. |
| 355 | + This is a randomized test: we pick two distinct indices |
| 356 | + and assert their mapped vertices differ.""" |
| 357 | + w, v, d, x1, x2 = params |
| 358 | + a1 = map_to_vertex(w, v, d, x1) |
| 359 | + a2 = map_to_vertex(w, v, d, x2) |
| 360 | + assert a1 != a2 |
0 commit comments