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 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}