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