polydat_nodes/sampling/
alias.rs1use std::collections::VecDeque;
16
17struct AliasSlot<T> {
22 bias: f64,
23 primary: T,
24 alias: T,
25}
26
27pub struct AliasTable<T> {
32 slots: Vec<AliasSlot<T>>,
33}
34
35impl<T: Clone> AliasTable<T> {
36 pub fn from_weights(outcomes: &[T], weights: &[f64]) -> Self {
42 assert_eq!(
43 outcomes.len(),
44 weights.len(),
45 "outcomes and weights must have equal length"
46 );
47 let n = outcomes.len();
48 assert!(n > 0, "must have at least one outcome");
49
50 let sum: f64 = weights.iter().sum();
51 assert!(sum > 0.0, "total weight must be positive");
52
53 let scale = n as f64 / sum;
55 let mut scaled: Vec<f64> = weights.iter().map(|w| w * scale).collect();
56
57 let mut small: VecDeque<usize> = VecDeque::new();
59 let mut large: VecDeque<usize> = VecDeque::new();
60 for (i, &w) in scaled.iter().enumerate() {
61 if w < 1.0 {
62 small.push_back(i);
63 } else {
64 large.push_back(i);
65 }
66 }
67
68 let mut slots: Vec<AliasSlot<T>> = (0..n)
70 .map(|i| AliasSlot {
71 bias: 1.0,
72 primary: outcomes[i].clone(),
73 alias: outcomes[i].clone(),
74 })
75 .collect();
76
77 while let (Some(s), Some(l)) = (small.pop_front(), large.pop_front()) {
78 slots[s].bias = scaled[s];
79 slots[s].alias = outcomes[l].clone();
80
81 scaled[l] -= 1.0 - scaled[s];
82 if scaled[l] < 1.0 {
83 small.push_back(l);
84 } else {
85 large.push_back(l);
86 }
87 }
88
89 for &i in small.iter().chain(large.iter()) {
91 slots[i].bias = 1.0;
92 }
93
94 Self { slots }
95 }
96
97 pub fn uniform(outcomes: &[T]) -> Self {
99 let weights = vec![1.0; outcomes.len()];
100 Self::from_weights(outcomes, &weights)
101 }
102
103 #[inline]
110 pub fn sample(&self, input: u64) -> &T {
111 let n = self.slots.len();
112 let slot_idx = (input as usize) % n;
113 let frac = (input >> 32) as f64 / u32::MAX as f64;
115 let slot = &self.slots[slot_idx];
116 if frac < slot.bias {
117 &slot.primary
118 } else {
119 &slot.alias
120 }
121 }
122
123 pub fn len(&self) -> usize {
125 self.slots.len()
126 }
127
128 pub fn is_empty(&self) -> bool {
130 self.slots.is_empty()
131 }
132}
133
134pub struct AliasTableU64 {
143 biases: Vec<f64>,
144 primaries: Vec<u64>,
145 aliases: Vec<u64>,
146}
147
148impl AliasTableU64 {
149 pub fn from_weights(weights: &[f64]) -> Self {
151 let n = weights.len();
152 assert!(n > 0, "must have at least one outcome");
153
154 let sum: f64 = weights.iter().sum();
155 assert!(sum > 0.0, "total weight must be positive");
156
157 let scale = n as f64 / sum;
158 let mut scaled: Vec<f64> = weights.iter().map(|w| w * scale).collect();
159
160 let mut small: VecDeque<usize> = VecDeque::new();
161 let mut large: VecDeque<usize> = VecDeque::new();
162 for (i, &w) in scaled.iter().enumerate() {
163 if w < 1.0 {
164 small.push_back(i);
165 } else {
166 large.push_back(i);
167 }
168 }
169
170 let mut biases = vec![1.0f64; n];
171 let primaries: Vec<u64> = (0..n as u64).collect();
172 let mut aliases: Vec<u64> = (0..n as u64).collect();
173
174 while let (Some(s), Some(l)) = (small.pop_front(), large.pop_front()) {
175 biases[s] = scaled[s];
176 aliases[s] = l as u64;
177
178 scaled[l] -= 1.0 - scaled[s];
179 if scaled[l] < 1.0 {
180 small.push_back(l);
181 } else {
182 large.push_back(l);
183 }
184 }
185
186 for &i in small.iter().chain(large.iter()) {
187 biases[i] = 1.0;
188 }
189
190 Self {
191 biases,
192 primaries,
193 aliases,
194 }
195 }
196
197 pub fn uniform(n: usize) -> Self {
199 Self::from_weights(&vec![1.0; n])
200 }
201
202 #[inline]
206 pub fn sample(&self, input: u64) -> u64 {
207 let n = self.biases.len();
208 let slot_idx = (input as usize) % n;
209 let frac = (input >> 32) as f64 / u32::MAX as f64;
210 if frac < self.biases[slot_idx] {
211 self.primaries[slot_idx]
212 } else {
213 self.aliases[slot_idx]
214 }
215 }
216
217 pub fn len(&self) -> usize {
219 self.biases.len()
220 }
221
222 pub fn is_empty(&self) -> bool {
224 self.biases.is_empty()
225 }
226
227 pub fn biases(&self) -> &[f64] {
229 &self.biases
230 }
231
232 pub fn primaries(&self) -> &[u64] {
234 &self.primaries
235 }
236
237 pub fn aliases(&self) -> &[u64] {
239 &self.aliases
240 }
241}
242
243use 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]
368 fn single_outcome() {
369 let table = AliasTableU64::from_weights(&[1.0]);
370 for i in 0..1000 {
371 assert_eq!(table.sample(i), 0);
372 }
373 }
374
375 #[test]
376 fn two_outcomes_50_50() {
377 use xxhash_rust::xxh3::xxh3_64;
378
379 let table = AliasTableU64::from_weights(&[1.0, 1.0]);
380 let mut counts = [0u64; 2];
381 let n = 100_000u64;
382 for i in 0..n {
383 let hashed = xxh3_64(&i.to_le_bytes());
384 counts[table.sample(hashed) as usize] += 1;
385 }
386 let ratio = counts[0] as f64 / n as f64;
387 assert!(
388 (0.40..0.60).contains(&ratio),
389 "expected ~50/50, got ratio {ratio}"
390 );
391 }
392}