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 disables the macro's auto-emitted u64
22//! kit; a `compiled_u64 = ...` / `jit_constants = ...` override, as
23//! `cycle_walk` uses, could capture it instead). Callers compute
24//! `feedback` via
25//! [`feedback_for_width_and_bank`] or [`feedback_for_size`].
26
27// -----------------------------------------------------------------
28// The algorithm
29// -----------------------------------------------------------------
30//
31// The LFSR step, the bounded permutation over it, and the feedback
32// polynomials all live in `polydat::numeric::permute`, which is where
33// a body shared by a node and its native lowering belongs: `shuffle`
34// below and `jit_shuffle` call the same function, so the interpreter
35// and native code agree by construction rather than by review. The
36// selection helpers keep their paths here.
37pub use polydat::numeric::permute::{
38 feedback_for_size, feedback_for_width, feedback_for_width_and_bank, width_for_period,
39};
40// Imported under the old name: `lfsr_step` is taken here by the
41// macro-consumed node-authoring function below.
42use polydat::numeric::permute::{lfsr_step as step, shuffle_bounded};
43
44// -----------------------------------------------------------------
45// Shuffle: bounded bijective permutation
46// -----------------------------------------------------------------
47
48/// Deterministic, bijective permutation of a bounded integer range.
49///
50/// Signature: `shuffle(input: u64, feedback: u64, size: u64, min: u64) -> (u64)`
51///
52/// Maps every value in [min, min+size) to itself in a pseudo-random
53/// order, visiting each value exactly once per cycle. Uses a Galois
54/// LFSR with rejection sampling to handle ranges that are not exact
55/// powers of 2.
56///
57/// Use when you need every key in a range visited exactly once without
58/// repetition and without materializing the full sequence in memory.
59/// Common patterns: generating unique primary keys for bulk inserts,
60/// distributing work across partitions without collision, or simulating
61/// a deck-of-cards draw. Pick different `feedback` polynomial values
62/// (via [`feedback_for_width_and_bank`]) for independent permutation
63/// orderings across distributed workers.
64///
65/// JIT level: P3 — every arg + return is `u64`, so the macro auto-emits
66/// `compiled_u64` with `feedback`/`size`/`min` captured by `Copy` and
67/// `jit_constants` returning `[feedback, size, min]` (the layout
68/// `JitOp::ShuffleConst` consumes).
69#[polydat::polydat_node(category = Permutation, validate = shuffle_validate)]
70fn shuffle(
71 input: u64,
72 #[poly_default(0u64)] feedback: Const<u64>,
73 #[poly_default(0u64)] size: Const<u64>,
74 #[poly_default(0u64)] min: Const<u64>,
75) -> u64 {
76 shuffle_bounded(input, *feedback, *size, *min)
77}
78
79/// `shuffle`'s range, `[min, min + size)`, must be a range of `u64`:
80/// its last value `min + size - 1` has to fit. The node answers inside
81/// the range, so for one past the top there is no right answer, and the
82/// denormalizing add overflowed (a panic in debug, a wrapped value in
83/// release). Node-level because no per-parameter constraint can relate
84/// two parameters.
85fn shuffle_validate(_name: &str, consts: &[polydat::dsl::factory::ConstArg]) -> Result<(), String> {
86 let size = consts.get(1).map(|c| c.as_u64()).unwrap_or(0);
87 let min = consts.get(2).map(|c| c.as_u64()).unwrap_or(0);
88 if size > 0 && min.checked_add(size - 1).is_none() {
89 return Err(format!(
90 "the range [min, min + size) must fit in u64; min {min} + size {size} runs past {}",
91 u64::MAX
92 ));
93 }
94 Ok(())
95}
96
97// -----------------------------------------------------------------
98// Raw LFSR step as a Polydat node (for advanced use)
99// -----------------------------------------------------------------
100
101/// Single Galois LFSR step as a Polydat node.
102///
103/// Signature: `lfsr_step(input: u64, feedback: u64) -> (u64)`
104///
105/// This is the raw bijective LFSR operation without range bounding.
106/// The period is 2^width - 1. Useful for building custom permutation
107/// patterns. The `feedback` polynomial selects which permutation
108/// ordering is produced — use [`feedback_for_width`] or
109/// [`feedback_for_width_and_bank`] to compute one for a given
110/// register width.
111///
112/// JIT level: P3 — auto-emitted because both args + return are `u64`.
113#[polydat::polydat_node(category = Permutation)]
114fn lfsr_step(input: u64, feedback: Const<u64>) -> u64 {
115 step(input, *feedback)
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use polydat::ast::{PolydatNode, Value};
122
123 /// `size` defaults to zero, so `shuffle(x)` and `shuffle(x, fb)`
124 /// reach the empty range without the author asking for it. It is
125 /// the range's floor, and above all it is not a panic: a division
126 /// by zero here would surface as a raw arithmetic message from the
127 /// build's constant fold.
128 #[test]
129 fn an_empty_range_answers_its_floor() {
130 for min in [0u64, 7, u64::MAX] {
131 let node = Shuffle::new(0, 0, min);
132 let mut out = [Value::None];
133 for input in [0u64, 1, 42, u64::MAX] {
134 node.eval(&[Value::U64(input)], &mut out);
135 assert_eq!(
136 out[0].as_u64(),
137 min,
138 "shuffle over an empty range at min={min}, input={input}"
139 );
140 }
141 }
142 }
143
144 #[test]
145 fn lfsr_step_nonzero() {
146 // LFSR should never produce 0 from a nonzero input
147 let feedback = feedback_for_width(8);
148 let mut reg = 1u64;
149 for _ in 0..255 {
150 reg = step(reg, feedback);
151 assert_ne!(reg, 0, "LFSR must never produce 0");
152 }
153 }
154
155 #[test]
156 fn lfsr_full_cycle() {
157 // An 8-bit LFSR should visit all 255 nonzero values exactly once
158 let feedback = feedback_for_width(8);
159 let mut seen = vec![false; 256];
160 let mut reg = 1u64;
161 for _ in 0..255 {
162 reg = step(reg, feedback);
163 assert!(!seen[reg as usize], "duplicate value {reg}");
164 seen[reg as usize] = true;
165 }
166 // Verify all nonzero values visited
167 for i in 1..=255u64 {
168 assert!(seen[i as usize], "value {i} not visited");
169 }
170 }
171
172 #[test]
173 fn lfsr_period_returns_to_start() {
174 let feedback = feedback_for_width(8);
175 let start = 42u64;
176 let mut reg = start;
177 for _ in 0..255 {
178 reg = step(reg, feedback);
179 }
180 assert_eq!(reg, start, "LFSR should return to start after 2^N-1 steps");
181 }
182
183 /// Test-only helper: build a `Shuffle` over `[min, min+size)` using
184 /// bank 0. Mirrors the historical `Shuffle::new(min, size)` shape so
185 /// the in-file tests stay readable.
186 fn shuf(min: u64, size: u64) -> Shuffle {
187 Shuffle::new(feedback_for_size(size), size, min)
188 }
189
190 /// Test-only helper: build a `Shuffle` over `[0, size)` using bank 0.
191 fn shuf0(size: u64) -> Shuffle {
192 shuf(0, size)
193 }
194
195 fn apply(node: &Shuffle, input: u64) -> u64 {
196 let mut out = [Value::None];
197 node.eval(&[Value::U64(input)], &mut out);
198 out[0].as_u64()
199 }
200
201 #[test]
202 fn shuffle_bijective_small() {
203 // Shuffle over [0, 31) should produce a permutation
204 let node = shuf0(31);
205 let mut seen = [false; 31];
206 for i in 0..31u64 {
207 let out = apply(&node, i);
208 assert!(out < 31, "out of range: {out}");
209 assert!(!seen[out as usize], "duplicate at input {i}: {out}");
210 seen[out as usize] = true;
211 }
212 assert!(seen.iter().all(|&s| s), "not all values produced");
213 }
214
215 #[test]
216 fn shuffle_bijective_non_power_of_two() {
217 // Shuffle over [0, 50) — not a power of 2, requires rejection sampling
218 let node = shuf0(50);
219 let mut seen = [false; 50];
220 for i in 0..50u64 {
221 let out = apply(&node, i);
222 assert!(out < 50, "out of range: {out}");
223 assert!(!seen[out as usize], "duplicate at input {i}: {out}");
224 seen[out as usize] = true;
225 }
226 assert!(seen.iter().all(|&s| s), "not all values produced");
227 }
228
229 #[test]
230 fn shuffle_with_min_offset() {
231 let node = shuf(100, 20);
232 let mut seen = [false; 20];
233 for i in 0..20u64 {
234 let out = apply(&node, i);
235 assert!((100..120).contains(&out), "out of range: {out}");
236 seen[(out - 100) as usize] = true;
237 }
238 assert!(seen.iter().all(|&s| s), "not all values produced");
239 }
240
241 #[test]
242 fn shuffle_deterministic() {
243 let node = shuf0(100);
244 let a = apply(&node, 42);
245 let b = apply(&node, 42);
246 assert_eq!(a, b);
247 }
248
249 #[test]
250 fn shuffle_not_identity() {
251 // The shuffle should reorder, not pass through
252 let node = shuf0(100);
253 let mut identity_count = 0;
254 for i in 0..100u64 {
255 if apply(&node, i) == i {
256 identity_count += 1;
257 }
258 }
259 // Some fixed points are expected, but not all
260 assert!(identity_count < 50, "shuffle should reorder most values");
261 }
262
263 #[test]
264 fn shuffle_polydat_node() {
265 let node = shuf0(100);
266 let mut out = [Value::None];
267 node.eval(&[Value::U64(7)], &mut out);
268 assert!(out[0].as_u64() < 100);
269 }
270
271 #[test]
272 fn shuffle_compiled() {
273 let node = shuf0(100);
274 let op = node.compiled_u64().expect("should compile");
275 let mut out = [0u64];
276 op(&[7], &mut out);
277 assert!(out[0] < 100);
278
279 // Matches eval path
280 let mut eval_out = [Value::None];
281 node.eval(&[Value::U64(7)], &mut eval_out);
282 assert_eq!(out[0], eval_out[0].as_u64());
283 }
284
285 #[test]
286 fn lfsr_step_node() {
287 let node = LfsrStep::new(feedback_for_width(8));
288 let mut out = [Value::None];
289 node.eval(&[Value::U64(1)], &mut out);
290 let v = out[0].as_u64();
291 assert_ne!(v, 0);
292 assert_ne!(v, 1);
293 }
294
295 #[test]
296 fn shuffle_large_range() {
297 // Verify shuffle works for a larger range (1000)
298 let node = shuf0(1000);
299 let mut seen = vec![false; 1000];
300 for i in 0..1000u64 {
301 let out = apply(&node, i);
302 assert!(out < 1000, "out of range: {out}");
303 seen[out as usize] = true;
304 }
305 assert!(seen.iter().all(|&s| s), "not all values produced");
306 }
307
308 #[test]
309 fn different_banks_different_orderings() {
310 let size = 100;
311 let fb0 = feedback_for_width_and_bank(width_for_period(size), 0);
312 let fb1 = feedback_for_width_and_bank(width_for_period(size), 1);
313 let n0 = Shuffle::new(fb0, size, 0);
314 let n1 = Shuffle::new(fb1, size, 0);
315 // Both should be bijective permutations
316 let mut seen0 = [false; 100];
317 let mut seen1 = [false; 100];
318 let mut differ = false;
319 for i in 0..100u64 {
320 let a = apply(&n0, i);
321 let b = apply(&n1, i);
322 assert!(a < 100);
323 assert!(b < 100);
324 seen0[a as usize] = true;
325 seen1[b as usize] = true;
326 if a != b {
327 differ = true;
328 }
329 }
330 assert!(seen0.iter().all(|&s| s), "bank 0 not bijective");
331 assert!(seen1.iter().all(|&s| s), "bank 1 not bijective");
332 assert!(differ, "different banks should produce different orderings");
333 }
334
335 #[test]
336 fn width_for_period_table() {
337 assert_eq!(width_for_period(1), 4); // minimum is 4
338 assert_eq!(width_for_period(15), 4); // 15 < 2^4
339 assert_eq!(width_for_period(16), 5); // 16 = 2^4, needs 5 bits
340 assert_eq!(width_for_period(31), 5);
341 assert_eq!(width_for_period(32), 6);
342 assert_eq!(width_for_period(255), 8);
343 assert_eq!(width_for_period(256), 9);
344 assert_eq!(width_for_period(1000), 10);
345 }
346}