Skip to main content

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}