11"""
2- Implements the mathematical operations for hypercube layers.
3-
4- This module provides the necessary functions to work with vertices in a
5- v-dimensional hypercube with coordinates in the range [0, w-1]. A key concept
6- is the partitioning of the hypercube's vertices into "layers". A vertex belongs
7- to layer `d`, where `d` is its distance from the sink vertex `(w-1, ..., w-1)`.
8-
9- The core functionalities are:
10- 1. **Precomputation and Caching**: Computes and caches the sizes
11- of each layer for different hypercube configurations (`w` and `v`).
12- 2. **Mapping**: Provides bijective mappings between an integer index within a
13- layer and the unique vertex (a list of coordinates) it represents.
2+ Implements the mathematical operations for hypercube-based encodings.
3+
4+ This module provides the core algorithms for working with vertices in a
5+ v-dimensional hypercube where coordinates are in the range [0, w-1]. This is a
6+ foundational component for the "Top of the Hypercube" signature schemes.
7+
8+ Core Concepts
9+ -------------
10+ 1. **Hypercube (`[w]^v`)**: The set of all possible coordinate vectors for a
11+ signature. The dimension `v` corresponds to the number of hash chains (and
12+ thus signature size), and the base `w` corresponds to the length of each
13+ hash chain.
14+
15+ 2. **Layers (`d`)**: The hypercube's vertices are partitioned into "layers" based
16+ on their **verification cost**. The layer `d` of a vertex is its distance
17+ from the sink vertex `(w-1, ..., w-1)`, calculated as:
18+ `d = (w-1)*v - sum(coordinates)`.
19+ A smaller `d` means a lower verification cost, making these "top layers"
20+ the most desirable for encoding messages into.
21+
22+ 3. **Mapping Problem**: The central challenge is to deterministically and
23+ efficiently map a single integer (derived from a message hash) to a unique
24+ coordinate vector within a specific layer or a set of top layers. This
25+ module provides the building blocks for that mapping.
1426
1527This logic is a direct translation of the algorithms described in the paper
1628"At the top of the hypercube" (eprint 2025/889)
2032from __future__ import annotations
2133
2234import bisect
35+ import math
2336from functools import lru_cache
37+ from itertools import accumulate
2438from typing import List , Tuple
2539
2640from pydantic import BaseModel , ConfigDict
3145
3246class LayerInfo (BaseModel ):
3347 """
34- Stores the precomputed sizes and cumulative sums for a
35- hypercube's layers.
48+ A data structure to store precomputed sizes and cumulative sums for the
49+ layers of a single hypercube configuration (fixed `w` and `v`).
50+
51+ This object makes subsequent calculations, like finding the total size of a
52+ range of layers, highly efficient.
3653 """
3754
3855 model_config = ConfigDict (frozen = True )
3956 sizes : List [int ]
40- """The number of vertices in each layer `d`."""
57+ """A list where `sizes[d]` is the number of vertices in layer `d`."""
4158 prefix_sums : List [int ]
4259 """
43- The cumulative number of vertices up to and including layer `d`.
60+ A list where `prefix_sums[d]` is the cumulative number of vertices from
61+ layer 0 up to and including layer `d`.
4462
45- `prefix_sums[d] = sizes[0] + ... + sizes[d]`.
63+ Mathematically: `prefix_sums[d] = sizes[0] + ... + sizes[d]`.
4664 """
4765
4866 def sizes_sum_in_range (self , start : int , end : int ) -> int :
49- """Calculates the sum of sizes in an inclusive range [start, end]."""
67+ """
68+ Calculates the sum of `sizes` in an inclusive range [start, end].
69+
70+ This is an O(1) operation thanks to the precomputed `prefix_sums`.
71+ """
72+ # If the range is invalid, the sum is zero.
5073 if start > end :
5174 return 0
75+ # If the range starts from the beginning, the sum is simply the
76+ # prefix sum at the end of the range.
5277 if start == 0 :
5378 return self .prefix_sums [end ]
79+ # Otherwise, the sum is the difference between the prefix sum at the
80+ # end and the prefix sum of the elements just before the start.
5481 else :
5582 return self .prefix_sums [end ] - self .prefix_sums [start - 1 ]
5683
5784
58- @lru_cache (maxsize = None )
59- def prepare_layer_info (w : int ) -> List [LayerInfo ]:
85+ def _calculate_layer_size (w : int , v : int , d : int ) -> int :
6086 """
61- Precomputes and caches the number of vertices in each layer of a hypercube.
87+ Calculates a hypercube layer's size using a direct combinatorial formula.
88+
89+ This function answers the question: "How many unique coordinate vectors
90+ (vertices) exist in a specific layer `d`?"
6291
63- It calculates the size of every layer for hypercubes with a given
64- base `w` (where coordinates are in `[0, w-1]`) for all dimensions
65- `v` up to `MAX_DIMENSION`.
92+ The problem is mathematically equivalent to finding the number of integer
93+ solutions to the equation:
94+ x_1 + x_2 + ... + x_v = k
95+ subject to the constraint that each coordinate `x_i` is in the range
96+ `0 <= x_i <= w-1`. The required sum `k` is derived from the layer's
97+ distance `d`.
6698
67- This precomputation is based on the recurrence relation from
68- Lemma 8 of the paper "At the top of the hypercube" (eprint 2025/889).
99+ The solution uses two key combinatorial techniques:
100+ 1. **Stars and Bars**: To find the number of solutions without the
101+ upper-bound constraint (`x_i <= w-1`).
102+
103+ 2. **Inclusion-Exclusion Principle**: To correct the count by systematically
104+ adding and subtracting the solutions that violate the upper bound.
69105
70106 Args:
71- w: The base of the hypercube.
107+ w: The hypercube base (coordinates are `0` to `w-1`).
108+ v: The hypercube dimension (number of coordinates).
109+ d: The target layer's distance from the sink vertex `(w-1, ..., w-1)`.
72110
73111 Returns:
74- A list where the element at index `v` is a `LayerInfo` object
75- containing the layer sizes for a `v`-dimensional hypercube.
112+ The total number of vertices in the specified layer.
76113 """
77- # Initialize a list to store the results for each dimension `v `.
114+ # A vertex is in layer `d` if its coordinates sum to `k = v * (w - 1) - d `.
78115 #
79- # Index 0 is unused to allow for direct indexing, e.g., `all_info[v]` .
80- all_info = [ LayerInfo ( sizes = [], prefix_sums = [])] * (MAX_DIMENSION + 1 )
116+ # This `coord_sum` is the `k` in our combinatorial problem .
117+ coord_sum = v * (w - 1 ) - d
81118
82- # BASE CASE
83- #
84- # For a 1-dimensional hypercube (v=1), which is just a line of `w` points
85- # with coordinates [0], [1], ..., [w-1].
119+ # This is the compact implementation of the inclusion-exclusion principle.
86120 #
87- # The distance `d` from the sink `[w-1]` is simply `(w-1) - coordinate`.
121+ # It directly calculates the sum: Σ (-1)^s * C(v,s) * C(k - s*w + v-1, v-1)
122+ return sum (
123+ ((- 1 ) ** s ) * math .comb (v , s ) * math .comb (coord_sum - s * w + v - 1 , v - 1 )
124+ for s in range (coord_sum // w + 1 )
125+ )
126+
127+
128+ @lru_cache (maxsize = None )
129+ def prepare_layer_info (w : int ) -> List [LayerInfo ]:
130+ """
131+ Precomputes and caches layer information using a direct combinatorial formula.
88132
89- # Each of the `w` possible layers contains exactly one vertex.
90- dim1_sizes = [1 ] * w
91- # The prefix sums (cumulative sizes) are therefore just [1, 2, 3, ..., w].
92- dim1_prefix_sums = list (range (1 , w + 1 ))
93- # Store the result for v=1, which will seed the inductive step.
94- all_info [1 ] = LayerInfo (sizes = dim1_sizes , prefix_sums = dim1_prefix_sums )
133+ For each dimension `v` up to `MAX_DIMENSION`, this function calculates the
134+ size of every layer `d` directly, without relying on the results from
135+ smaller dimensions. While less computationally efficient than the recursive
136+ method, this implementation is more concise and mathematically direct.
137+
138+ Args:
139+ w: The base of the hypercube.
140+
141+ Returns:
142+ A list where `list[v]` is a `LayerInfo` object for a `v`-dim hypercube.
143+ """
144+ all_info = [LayerInfo (sizes = [], prefix_sums = [])] * (MAX_DIMENSION + 1 )
95145
96- # Now, build the layer info for all higher dimensions up to the maximum.
97- for v in range (2 , MAX_DIMENSION + 1 ):
146+ for v in range (1 , MAX_DIMENSION + 1 ):
98147 # The maximum possible distance `d` in a v-dimensional hypercube.
99148 max_d = (w - 1 ) * v
100- # Retrieve the already-computed data for the previous dimension (v-1).
101- prev_layer_info = all_info [v - 1 ]
102-
103- # This list will store the computed size of each layer `d`
104- # for dimension `v`.
105- current_sizes : List [int ] = []
106- for d in range (max_d + 1 ):
107- # Implements the recurrence l_d(v) = Σ l_{d-j}(v-1) from the paper.
108- # `j` is one coordinate's distance contribution.
109-
110- # Calculate the valid range [j_min, j_max] for `j`.
111- j_min = max (0 , d - (w - 1 ) * (v - 1 ))
112- j_max = min (w - 1 , d )
113-
114- # Translate the sum over `j` to an index range `k`,
115- # where k = d - j.
116- #
117- # This allows for an efficient lookup using prefix sums.
118- k_min = d - j_max
119- k_max = d - j_min
120-
121- # Calculate the sum using the precomputed prefix sums
122- # from the previous dimension's `LayerInfo`.
123- layer_size = prev_layer_info .sizes_sum_in_range (k_min , k_max )
124- current_sizes .append (layer_size )
125-
126- # After computing all layer sizes for dimension `v`, we compute their
127- # prefix sums.
128- #
129- # This is needed for the *next* iteration (for dimension v+1).
130- current_prefix_sums : List [int ] = []
131- current_sum = 0
132- for size in current_sizes :
133- current_sum += size
134- current_prefix_sums .append (current_sum )
149+
150+ # Directly compute the size of each layer using the helper function.
151+ sizes = [_calculate_layer_size (w , v , d ) for d in range (max_d + 1 )]
152+
153+ # Compute the cumulative sums from the list of sizes.
154+ prefix_sums = list (accumulate (sizes ))
135155
136156 # Store the complete layer info for the current dimension `v`.
137- all_info [v ] = LayerInfo (sizes = current_sizes , prefix_sums = current_prefix_sums )
157+ all_info [v ] = LayerInfo (sizes = sizes , prefix_sums = prefix_sums )
138158
139- # Return the complete table of layer information for the given base `w`.
140159 return all_info
141160
142161
@@ -152,16 +171,18 @@ def hypercube_part_size(w: int, v: int, d: int) -> int:
152171
153172def hypercube_find_layer (w : int , v : int , x : int ) -> Tuple [int , int ]:
154173 """
155- Given a global index `x`, finds the layer `d` it belongs to and its
156- local index (`remainder`) within that layer.
174+ Given a global index `x`, finds its layer `d` and local offset `remainder`.
175+
176+ This function determines which "layer bucket" a global index falls into.
157177
158178 Args:
159179 w: The hypercube base.
160180 v: The hypercube dimension.
161- x: The global index of a vertex ( from 0 to w**v - 1).
181+ x: The global index of a vertex, from 0 to ( w**v - 1).
162182
163183 Returns:
164- A tuple `(d, remainder)`.
184+ A tuple `(d, remainder)`, where `d` is the layer and `remainder` is
185+ the local index (offset) of the vertex within that layer.
165186 """
166187 prefix_sums = prepare_layer_info (w )[v ].prefix_sums
167188 # Use binary search to efficiently find the correct layer.
@@ -187,17 +208,16 @@ def map_to_vertex(w: int, v: int, d: int, x: int) -> List[int]:
187208 """
188209 Maps an integer index `x` to a unique vertex in a specific hypercube layer.
189210
190- This function provides a bijective mapping from an integer `x`
191- (derived from a hash) to a unique list of `v` coordinates,
192- `[a_0, ..., a_{v-1}]`.
193-
194- The algorithm works iteratively, determining one coordinate at a time.
211+ This function provides a bijective mapping from a location `(d, x)` to a
212+ unique coordinate vector `[a_0, ..., a_{v-1}]`. The algorithm works
213+ iteratively, determining one coordinate at a time by reducing the problem
214+ to a smaller subproblem in a hypercube of one less dimension.
195215
196216 Args:
197217 w: The hypercube base (coordinates are in `[0, w-1]`).
198218 v: The hypercube dimension (the number of coordinates).
199219 d: The target layer, defined by its distance from the sink vertex.
200- x: The integer index within the layer `d`, must be `0 <= x < size(d)`.
220+ x: The integer index (offset) within layer `d`. Must be `0 <= x < size(d)`.
201221
202222 Returns:
203223 A list of `v` integers representing the coordinates of the vertex.
@@ -218,8 +238,7 @@ def map_to_vertex(w: int, v: int, d: int, x: int) -> List[int]:
218238 dim_remaining = v - i
219239 prev_dim_layer_info = layer_info_cache [dim_remaining ]
220240
221- # This loop finds which block of sub-hypercubes the index `x_curr`
222- # falls into.
241+ # This loop finds which block of sub-hypercubes the index `x_curr` falls into.
223242 #
224243 # It skips over full blocks by subtracting their size
225244 # from `x_curr` until the correct one is found.
@@ -230,7 +249,8 @@ def map_to_vertex(w: int, v: int, d: int, x: int) -> List[int]:
230249 if x_curr >= count :
231250 x_curr -= count
232251 else :
233- ji = j # Found the correct block.
252+ # Found the correct block.
253+ ji = j
234254 break
235255
236256 if ji == - 1 :
0 commit comments