polydat_nodes/sampling/
alias.rs1use std::collections::VecDeque;
17
18struct AliasSlot<T> {
23 bias: f64,
24 primary: T,
25 alias: T,
26}
27
28pub struct AliasTable<T> {
33 slots: Vec<AliasSlot<T>>,
34}
35
36impl<T: Clone> AliasTable<T> {
37 pub fn from_weights(outcomes: &[T], weights: &[f64]) -> Self {
43 assert_eq!(
44 outcomes.len(),
45 weights.len(),
46 "outcomes and weights must have equal length"
47 );
48 let n = outcomes.len();
49 assert!(n > 0, "must have at least one outcome");
50
51 let sum: f64 = weights.iter().sum();
52 assert!(sum > 0.0, "total weight must be positive");
53
54 let scale = n as f64 / sum;
56 let mut scaled: Vec<f64> = weights.iter().map(|w| w * scale).collect();
57
58 let mut small: VecDeque<usize> = VecDeque::new();
60 let mut large: VecDeque<usize> = VecDeque::new();
61 for (i, &w) in scaled.iter().enumerate() {
62 if w < 1.0 {
63 small.push_back(i);
64 } else {
65 large.push_back(i);
66 }
67 }
68
69 let mut slots: Vec<AliasSlot<T>> = (0..n)
71 .map(|i| AliasSlot {
72 bias: 1.0,
73 primary: outcomes[i].clone(),
74 alias: outcomes[i].clone(),
75 })
76 .collect();
77
78 while let (Some(s), Some(l)) = (small.pop_front(), large.pop_front()) {
79 slots[s].bias = scaled[s];
80 slots[s].alias = outcomes[l].clone();
81
82 scaled[l] -= 1.0 - scaled[s];
83 if scaled[l] < 1.0 {
84 small.push_back(l);
85 } else {
86 large.push_back(l);
87 }
88 }
89
90 for &i in small.iter().chain(large.iter()) {
92 slots[i].bias = 1.0;
93 }
94
95 Self { slots }
96 }
97
98 pub fn uniform(outcomes: &[T]) -> Self {
100 let weights = vec![1.0; outcomes.len()];
101 Self::from_weights(outcomes, &weights)
102 }
103
104 #[inline]
111 pub fn sample(&self, input: u64) -> &T {
112 let n = self.slots.len();
113 let slot_idx = (input as usize) % n;
114 let frac = (input >> 32) as f64 / u32::MAX as f64;
116 let slot = &self.slots[slot_idx];
117 if frac < slot.bias {
118 &slot.primary
119 } else {
120 &slot.alias
121 }
122 }
123
124 pub fn len(&self) -> usize {
126 self.slots.len()
127 }
128
129 pub fn is_empty(&self) -> bool {
131 self.slots.is_empty()
132 }
133}
134
135pub struct AliasTableU64 {
144 biases: Vec<f64>,
145 primaries: Vec<u64>,
146 aliases: Vec<u64>,
147}
148
149impl AliasTableU64 {
150 pub fn from_weights(weights: &[f64]) -> Self {
152 let n = weights.len();
153 assert!(n > 0, "must have at least one outcome");
154
155 let sum: f64 = weights.iter().sum();
156 assert!(sum > 0.0, "total weight must be positive");
157
158 let scale = n as f64 / sum;
159 let mut scaled: Vec<f64> = weights.iter().map(|w| w * scale).collect();
160
161 let mut small: VecDeque<usize> = VecDeque::new();
162 let mut large: VecDeque<usize> = VecDeque::new();
163 for (i, &w) in scaled.iter().enumerate() {
164 if w < 1.0 {
165 small.push_back(i);
166 } else {
167 large.push_back(i);
168 }
169 }
170
171 let mut biases = vec![1.0f64; n];
172 let primaries: Vec<u64> = (0..n as u64).collect();
173 let mut aliases: Vec<u64> = (0..n as u64).collect();
174
175 while let (Some(s), Some(l)) = (small.pop_front(), large.pop_front()) {
176 biases[s] = scaled[s];
177 aliases[s] = l as u64;
178
179 scaled[l] -= 1.0 - scaled[s];
180 if scaled[l] < 1.0 {
181 small.push_back(l);
182 } else {
183 large.push_back(l);
184 }
185 }
186
187 for &i in small.iter().chain(large.iter()) {
188 biases[i] = 1.0;
189 }
190
191 Self {
192 biases,
193 primaries,
194 aliases,
195 }
196 }
197
198 pub fn uniform(n: usize) -> Self {
200 Self::from_weights(&vec![1.0; n])
201 }
202
203 #[inline]
207 pub fn sample(&self, input: u64) -> u64 {
208 let n = self.biases.len();
209 let slot_idx = (input as usize) % n;
210 let frac = (input >> 32) as f64 / u32::MAX as f64;
211 if frac < self.biases[slot_idx] {
212 self.primaries[slot_idx]
213 } else {
214 self.aliases[slot_idx]
215 }
216 }
217
218 pub fn len(&self) -> usize {
220 self.biases.len()
221 }
222
223 pub fn is_empty(&self) -> bool {
225 self.biases.is_empty()
226 }
227
228 pub fn biases(&self) -> &[f64] {
230 &self.biases
231 }
232
233 pub fn primaries(&self) -> &[u64] {
235 &self.primaries
236 }
237
238 pub fn aliases(&self) -> &[u64] {
240 &self.aliases
241 }
242}
243
244use polydat::derive_support::PolydatSetup;
255
256impl PolydatSetup for AliasTableU64 {}
257
258fn build_alias_table(weights: &[f64]) -> AliasTableU64 {
261 AliasTableU64::from_weights(weights)
262}
263
264#[polydat::polydat_node(category = Probability)]
269fn alias_sample(
270 input: u64,
271 weights: Const<Vec<f64>>,
272 #[poly_const(build_alias_table, from = weights)] table: &AliasTableU64,
273) -> u64 {
274 let _ = weights;
275 table.sample(input)
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use polydat::ast::Value;
282
283 #[test]
284 fn uniform_table_all_outcomes_reachable() {
285 use xxhash_rust::xxh3::xxh3_64;
286
287 let table = AliasTableU64::uniform(4);
288 let mut seen = [false; 4];
289 for i in 0..10_000u64 {
290 let hashed = xxh3_64(&i.to_le_bytes());
291 let outcome = table.sample(hashed) as usize;
292 assert!(outcome < 4, "outcome {outcome} out of range");
293 seen[outcome] = true;
294 }
295 for (i, &s) in seen.iter().enumerate() {
296 assert!(s, "outcome {i} was never sampled");
297 }
298 }
299
300 #[test]
301 fn weighted_table_respects_distribution() {
302 use xxhash_rust::xxh3::xxh3_64;
303
304 let table = AliasTableU64::from_weights(&[100.0, 1.0, 1.0]);
308 let mut counts = [0u64; 3];
309 let n = 100_000u64;
310 for i in 0..n {
311 let hashed = xxh3_64(&i.to_le_bytes());
312 counts[table.sample(hashed) as usize] += 1;
313 }
314 let ratio = counts[0] as f64 / n as f64;
316 assert!(
317 ratio > 0.90,
318 "expected outcome 0 to dominate, got ratio {ratio} (counts: {counts:?})"
319 );
320 }
321
322 #[test]
323 fn deterministic() {
324 let table = AliasTableU64::from_weights(&[1.0, 2.0, 3.0]);
325 let a = table.sample(42);
326 let b = table.sample(42);
327 assert_eq!(a, b, "same input must produce same output");
328 }
329
330 #[test]
331 fn generic_table_strings() {
332 use xxhash_rust::xxh3::xxh3_64;
333
334 let outcomes = vec!["alpha", "beta", "gamma"];
335 let weights = vec![1.0, 1.0, 1.0];
336 let table = AliasTable::from_weights(&outcomes, &weights);
337 let mut seen = [false; 3];
338 for i in 0..10_000u64 {
339 let hashed = xxh3_64(&i.to_le_bytes());
340 let result = *table.sample(hashed);
341 match result {
342 "alpha" => seen[0] = true,
343 "beta" => seen[1] = true,
344 "gamma" => seen[2] = true,
345 other => panic!("unexpected outcome: {other}"),
346 }
347 }
348 for (i, &s) in seen.iter().enumerate() {
349 assert!(s, "outcome {i} never seen");
350 }
351 }
352
353 #[test]
354 fn polydat_node_eval() {
355 use polydat::ast::PolydatNode;
356 let node = AliasSample::new(vec![1.0, 1.0, 1.0, 1.0]);
357 let mut out = [Value::None];
358 node.eval(&[Value::U64(42)], &mut out);
359 assert!(out[0].as_u64() < 4);
360 }
361
362 #[test]
367 fn single_outcome() {
368 let table = AliasTableU64::from_weights(&[1.0]);
369 for i in 0..1000 {
370 assert_eq!(table.sample(i), 0);
371 }
372 }
373
374 #[test]
375 fn two_outcomes_50_50() {
376 use xxhash_rust::xxh3::xxh3_64;
377
378 let table = AliasTableU64::from_weights(&[1.0, 1.0]);
379 let mut counts = [0u64; 2];
380 let n = 100_000u64;
381 for i in 0..n {
382 let hashed = xxh3_64(&i.to_le_bytes());
383 counts[table.sample(hashed) as usize] += 1;
384 }
385 let ratio = counts[0] as f64 / n as f64;
386 assert!(
387 (0.40..0.60).contains(&ratio),
388 "expected ~50/50, got ratio {ratio}"
389 );
390 }
391}