polydat_nodes/sampling/metashift.rs
1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Galois LFSR-based deterministic permutation (MetaShift / Shuffle).
5//!
6//! Provides bijective, deterministic, O(1)-space permutations of integer
7//! ranges. Given a range [0, N), the LFSR visits every value exactly once
8//! before cycling, in a pseudo-random order determined by the feedback
9//! polynomial.
10//!
11//! This is useful for:
12//! - Generating all values in a range without repetition or memory
13//! - Shuffling sequences without materializing them
14//! - Deterministic reordering across distributed workers (via bank selection)
15//!
16//! The core algorithm is a Galois-configuration LFSR. The `Shuffle` Polydat
17//! node wraps it with range normalization and rejection sampling.
18//!
19//! The `feedback` polynomial is exposed explicitly as a Const arg so the
20//! macro can auto-emit the JIT-eligible `compiled_u64` / `jit_constants`
21//! hooks (Setup-derived state would disable the macro's auto-JIT
22//! emission, and the override path can't capture per-instance
23//! constants). Callers compute `feedback` via
24//! [`feedback_for_width_and_bank`] or [`feedback_for_size`].
25
26// -----------------------------------------------------------------
27// LFSR feedback polynomials (one per register width 4..64)
28// -----------------------------------------------------------------
29
30/// Number of banks (feedback polynomials) stored per register width.
31const BANKS_PER_WIDTH: usize = 8;
32
33/// Galois LFSR feedback polynomials, 8 banks per register width 4..64.
34/// Indexed as FEEDBACK_BANKS[(width - 4) * 8 + bank].
35/// Widths with fewer than 8 known polynomials repeat the last one.
36const FEEDBACK_BANKS: [u64; 61 * BANKS_PER_WIDTH] = include!("metashift_banks.inc");
37
38/// Return the feedback polynomial for a given register width and bank.
39///
40/// `width` must be 4..=64. `bank` selects among different polynomials
41/// for the same width (modulo the number of available banks). Different
42/// banks produce different permutation orderings over the same range.
43pub fn feedback_for_width_and_bank(width: u32, bank: usize) -> u64 {
44 assert!(
45 (4..=64).contains(&width),
46 "LFSR width must be 4..64, got {width}"
47 );
48 let base = (width as usize - 4) * BANKS_PER_WIDTH;
49 FEEDBACK_BANKS[base + (bank % BANKS_PER_WIDTH)]
50}
51
52/// Return the default (bank 0) feedback polynomial for a given width.
53pub fn feedback_for_width(width: u32) -> u64 {
54 feedback_for_width_and_bank(width, 0)
55}
56
57/// Return the minimum register width needed to represent `period` values.
58pub fn width_for_period(period: u64) -> u32 {
59 assert!(period > 0, "period must be positive");
60 let bits = 64 - period.leading_zeros();
61 bits.max(4) // minimum 4-bit LFSR
62}
63
64/// Convenience: derive a bank-0 feedback polynomial directly from a
65/// shuffle `size`. Callers building a `Shuffle` node from an outer
66/// "size" parameter use this rather than tracking width / bank
67/// manually.
68pub fn feedback_for_size(size: u64) -> u64 {
69 feedback_for_width_and_bank(width_for_period(size), 0)
70}
71
72// -----------------------------------------------------------------
73// Core LFSR step (algorithm)
74// -----------------------------------------------------------------
75
76/// Single Galois LFSR step.
77///
78/// This is the fundamental bijective operation: given a register value,
79/// produce the next value in the LFSR sequence. The helper is named
80/// `step` (not `lfsr_step`) to avoid colliding with the macro-consumed
81/// `fn lfsr_step` node-authoring function below.
82#[inline]
83fn step(register: u64, feedback: u64) -> u64 {
84 let lsb = register & 1;
85 let shifted = register >> 1;
86 // If LSB was 1, XOR with feedback polynomial; otherwise just shift.
87 // The (-lsb) trick: if lsb=1, -1u64 = all 1s (mask passes feedback);
88 // if lsb=0, 0u64 (mask blocks feedback).
89 shifted ^ (lsb.wrapping_neg() & feedback)
90}
91
92// -----------------------------------------------------------------
93// Shuffle: bounded bijective permutation
94// -----------------------------------------------------------------
95
96/// Deterministic, bijective permutation of a bounded integer range.
97///
98/// Signature: `shuffle(input: u64, feedback: u64, size: u64, min: u64) -> (u64)`
99///
100/// Maps every value in [min, min+size) to itself in a pseudo-random
101/// order, visiting each value exactly once per cycle. Uses a Galois
102/// LFSR with rejection sampling to handle ranges that are not exact
103/// powers of 2.
104///
105/// Use when you need every key in a range visited exactly once without
106/// repetition and without materializing the full sequence in memory.
107/// Common patterns: generating unique primary keys for bulk inserts,
108/// distributing work across partitions without collision, or simulating
109/// a deck-of-cards draw. Pick different `feedback` polynomial values
110/// (via [`feedback_for_width_and_bank`]) for independent permutation
111/// orderings across distributed workers.
112///
113/// JIT level: P3 — every arg + return is `u64`, so the macro auto-emits
114/// `compiled_u64` with `feedback`/`size`/`min` captured by `Copy` and
115/// `jit_constants` returning `[feedback, size, min]` (the layout
116/// `JitOp::ShuffleConst` consumes).
117#[polydat::polydat_node(category = Permutation)]
118fn shuffle(
119 input: u64,
120 #[poly_default(0u64)] feedback: Const<u64>,
121 #[poly_default(0u64)] size: Const<u64>,
122 #[poly_default(0u64)] min: Const<u64>,
123) -> u64 {
124 // Normalize to 1-based LFSR range (LFSR cannot produce 0)
125 let mut register = (input % *size) + 1;
126
127 // Apply LFSR with rejection sampling: if result exceeds size,
128 // step again until it's in range.
129 loop {
130 register = step(register, *feedback);
131 if register <= *size {
132 break;
133 }
134 }
135
136 // Denormalize back to [min, min+size)
137 (register - 1) + *min
138}
139
140// -----------------------------------------------------------------
141// Raw LFSR step as a Polydat node (for advanced use)
142// -----------------------------------------------------------------
143
144/// Single Galois LFSR step as a Polydat node.
145///
146/// Signature: `lfsr_step(input: u64, feedback: u64) -> (u64)`
147///
148/// This is the raw bijective LFSR operation without range bounding.
149/// The period is 2^width - 1. Useful for building custom permutation
150/// patterns. The `feedback` polynomial selects which permutation
151/// ordering is produced — use [`feedback_for_width`] or
152/// [`feedback_for_width_and_bank`] to compute one for a given
153/// register width.
154///
155/// JIT level: P3 — auto-emitted because both args + return are `u64`.
156#[polydat::polydat_node(category = Permutation)]
157fn lfsr_step(input: u64, feedback: Const<u64>) -> u64 {
158 step(input, *feedback)
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use polydat::ast::{PolydatNode, Value};
165
166 #[test]
167 fn lfsr_step_nonzero() {
168 // LFSR should never produce 0 from a nonzero input
169 let feedback = feedback_for_width(8);
170 let mut reg = 1u64;
171 for _ in 0..255 {
172 reg = step(reg, feedback);
173 assert_ne!(reg, 0, "LFSR must never produce 0");
174 }
175 }
176
177 #[test]
178 fn lfsr_full_cycle() {
179 // An 8-bit LFSR should visit all 255 nonzero values exactly once
180 let feedback = feedback_for_width(8);
181 let mut seen = vec![false; 256];
182 let mut reg = 1u64;
183 for _ in 0..255 {
184 reg = step(reg, feedback);
185 assert!(!seen[reg as usize], "duplicate value {reg}");
186 seen[reg as usize] = true;
187 }
188 // Verify all nonzero values visited
189 for i in 1..=255u64 {
190 assert!(seen[i as usize], "value {i} not visited");
191 }
192 }
193
194 #[test]
195 fn lfsr_period_returns_to_start() {
196 let feedback = feedback_for_width(8);
197 let start = 42u64;
198 let mut reg = start;
199 for _ in 0..255 {
200 reg = step(reg, feedback);
201 }
202 assert_eq!(reg, start, "LFSR should return to start after 2^N-1 steps");
203 }
204
205 /// Test-only helper: build a `Shuffle` over `[min, min+size)` using
206 /// bank 0. Mirrors the historical `Shuffle::new(min, size)` shape so
207 /// the in-file tests stay readable.
208 fn shuf(min: u64, size: u64) -> Shuffle {
209 Shuffle::new(feedback_for_size(size), size, min)
210 }
211
212 /// Test-only helper: build a `Shuffle` over `[0, size)` using bank 0.
213 fn shuf0(size: u64) -> Shuffle {
214 shuf(0, size)
215 }
216
217 fn apply(node: &Shuffle, input: u64) -> u64 {
218 let mut out = [Value::None];
219 node.eval(&[Value::U64(input)], &mut out);
220 out[0].as_u64()
221 }
222
223 #[test]
224 fn shuffle_bijective_small() {
225 // Shuffle over [0, 31) should produce a permutation
226 let node = shuf0(31);
227 let mut seen = [false; 31];
228 for i in 0..31u64 {
229 let out = apply(&node, i);
230 assert!(out < 31, "out of range: {out}");
231 assert!(!seen[out as usize], "duplicate at input {i}: {out}");
232 seen[out as usize] = true;
233 }
234 assert!(seen.iter().all(|&s| s), "not all values produced");
235 }
236
237 #[test]
238 fn shuffle_bijective_non_power_of_two() {
239 // Shuffle over [0, 50) — not a power of 2, requires rejection sampling
240 let node = shuf0(50);
241 let mut seen = [false; 50];
242 for i in 0..50u64 {
243 let out = apply(&node, i);
244 assert!(out < 50, "out of range: {out}");
245 assert!(!seen[out as usize], "duplicate at input {i}: {out}");
246 seen[out as usize] = true;
247 }
248 assert!(seen.iter().all(|&s| s), "not all values produced");
249 }
250
251 #[test]
252 fn shuffle_with_min_offset() {
253 let node = shuf(100, 20);
254 let mut seen = [false; 20];
255 for i in 0..20u64 {
256 let out = apply(&node, i);
257 assert!((100..120).contains(&out), "out of range: {out}");
258 seen[(out - 100) as usize] = true;
259 }
260 assert!(seen.iter().all(|&s| s), "not all values produced");
261 }
262
263 #[test]
264 fn shuffle_deterministic() {
265 let node = shuf0(100);
266 let a = apply(&node, 42);
267 let b = apply(&node, 42);
268 assert_eq!(a, b);
269 }
270
271 #[test]
272 fn shuffle_not_identity() {
273 // The shuffle should reorder, not pass through
274 let node = shuf0(100);
275 let mut identity_count = 0;
276 for i in 0..100u64 {
277 if apply(&node, i) == i {
278 identity_count += 1;
279 }
280 }
281 // Some fixed points are expected, but not all
282 assert!(identity_count < 50, "shuffle should reorder most values");
283 }
284
285 #[test]
286 fn shuffle_polydat_node() {
287 let node = shuf0(100);
288 let mut out = [Value::None];
289 node.eval(&[Value::U64(7)], &mut out);
290 assert!(out[0].as_u64() < 100);
291 }
292
293 #[test]
294 fn shuffle_compiled() {
295 let node = shuf0(100);
296 let op = node.compiled_u64().expect("should compile");
297 let mut out = [0u64];
298 op(&[7], &mut out);
299 assert!(out[0] < 100);
300
301 // Matches eval path
302 let mut eval_out = [Value::None];
303 node.eval(&[Value::U64(7)], &mut eval_out);
304 assert_eq!(out[0], eval_out[0].as_u64());
305 }
306
307 #[test]
308 fn lfsr_step_node() {
309 let node = LfsrStep::new(feedback_for_width(8));
310 let mut out = [Value::None];
311 node.eval(&[Value::U64(1)], &mut out);
312 let v = out[0].as_u64();
313 assert_ne!(v, 0);
314 assert_ne!(v, 1);
315 }
316
317 #[test]
318 fn shuffle_large_range() {
319 // Verify shuffle works for a larger range (1000)
320 let node = shuf0(1000);
321 let mut seen = vec![false; 1000];
322 for i in 0..1000u64 {
323 let out = apply(&node, i);
324 assert!(out < 1000, "out of range: {out}");
325 seen[out as usize] = true;
326 }
327 assert!(seen.iter().all(|&s| s), "not all values produced");
328 }
329
330 #[test]
331 fn different_banks_different_orderings() {
332 let size = 100;
333 let fb0 = feedback_for_width_and_bank(width_for_period(size), 0);
334 let fb1 = feedback_for_width_and_bank(width_for_period(size), 1);
335 let n0 = Shuffle::new(fb0, size, 0);
336 let n1 = Shuffle::new(fb1, size, 0);
337 // Both should be bijective permutations
338 let mut seen0 = [false; 100];
339 let mut seen1 = [false; 100];
340 let mut differ = false;
341 for i in 0..100u64 {
342 let a = apply(&n0, i);
343 let b = apply(&n1, i);
344 assert!(a < 100);
345 assert!(b < 100);
346 seen0[a as usize] = true;
347 seen1[b as usize] = true;
348 if a != b {
349 differ = true;
350 }
351 }
352 assert!(seen0.iter().all(|&s| s), "bank 0 not bijective");
353 assert!(seen1.iter().all(|&s| s), "bank 1 not bijective");
354 assert!(differ, "different banks should produce different orderings");
355 }
356
357 #[test]
358 fn width_for_period_table() {
359 assert_eq!(width_for_period(1), 4); // minimum is 4
360 assert_eq!(width_for_period(15), 4); // 15 < 2^4
361 assert_eq!(width_for_period(16), 5); // 16 = 2^4, needs 5 bits
362 assert_eq!(width_for_period(31), 5);
363 assert_eq!(width_for_period(32), 6);
364 assert_eq!(width_for_period(255), 8);
365 assert_eq!(width_for_period(256), 9);
366 assert_eq!(width_for_period(1000), 10);
367 }
368}