use rust_decimal::Decimal;
use rand::Rng;
use roaring::RoaringBitmap;
use std::collections::HashSet;
use std::vec;
const DEFAULT_PRECISION: u8 = 3;
#[derive(Debug, Clone)]
pub enum NodeContent {
Internal(Vec<Node>),
Leaf(RoaringBitmap),
}
#[derive(Debug, Clone)]
pub struct Node {
pub content: NodeContent,
pub accumulated_value: Decimal,
pub content_count: u32,
}
impl Node {
fn new_internal() -> Self {
Self {
content: NodeContent::Internal(vec![]),
accumulated_value: Decimal::from(0),
content_count: 0,
}
}
}
#[derive(Debug)]
pub struct DigitBinIndex {
pub root: Node,
pub precision: u8,
}
impl Default for DigitBinIndex {
fn default() -> Self {
Self::new()
}
}
impl DigitBinIndex {
#[must_use]
pub fn new() -> Self {
Self::with_precision(DEFAULT_PRECISION)
}
#[must_use]
pub fn with_precision(precision: u8) -> Self {
assert!(precision > 0, "Precision must be at least 1.");
Self {
root: Node::new_internal(),
precision,
}
}
fn get_digit_at(weight: Decimal, position: u8) -> usize {
let position = position as u32;
let scale = weight.scale();
if position > scale {
return 0;
}
let mantissa = weight.mantissa().abs() as u128;
let power_of_10 = 10u128.pow(scale - position);
let digit = (mantissa / power_of_10) % 10;
digit as usize
}
pub fn add(&mut self, individual_id: u32, weight: Decimal) {
Self::add_recurse(&mut self.root, individual_id, weight, 1, self.precision);
}
fn add_recurse(
node: &mut Node,
individual_id: u32,
weight: Decimal,
current_depth: u8,
max_depth: u8,
) {
node.content_count += 1;
node.accumulated_value += weight;
if current_depth > max_depth {
match &mut node.content {
NodeContent::Leaf(bitmap) => { bitmap.insert(individual_id); },
NodeContent::Internal(children) => {
if children.is_empty() {
node.content = NodeContent::Leaf(RoaringBitmap::from_iter([individual_id]));
} else { panic!("Cannot add individual to a non-empty internal node at leaf depth."); }
}
}
return;
}
let digit = Self::get_digit_at(weight, current_depth);
if let NodeContent::Internal(children) = &mut node.content {
if children.len() <= digit {
children.resize_with(digit + 1, Node::new_internal);
}
Self::add_recurse(&mut children[digit], individual_id, weight, current_depth + 1, max_depth);
} else {
panic!("Attempted to traverse deeper on what should be a leaf node.");
}
}
pub fn select_and_remove(&mut self) -> Option<(u32, Decimal)> {
if self.root.content_count == 0 {
return None;
}
let mut rng = rand::thread_rng();
let random_target = rng.gen_range(Decimal::ZERO..self.root.accumulated_value);
let (selected_id, weight, path) = Self::select_recurse(&mut self.root, random_target, vec![]);
self.update_values_post_removal(&path, weight);
Some((selected_id, weight))
}
pub fn select_many_and_remove(&mut self, num_to_draw: u32) -> Option<HashSet<u32>> {
if num_to_draw > self.count() { return None; }
if num_to_draw == 0 { return Some(HashSet::new()); }
let mut selected_items = Vec::with_capacity(num_to_draw as usize);
let mut selected_ids = HashSet::with_capacity(num_to_draw as usize);
let mut rng = rand::thread_rng();
while selected_ids.len() < num_to_draw as usize {
let random_target = rng.gen_range(Decimal::ZERO..self.root.accumulated_value);
if let Some((id, weight, path)) = self.find_candidate_recurse(&self.root, random_target, vec![]) {
if selected_ids.insert(id) {
selected_items.push((path, id, weight));
}
} else {
return None;
}
}
for (path, id, weight) in selected_items {
Self::update_and_remove_recurse(&mut self.root, &path, id, weight);
}
Some(selected_ids)
}
fn find_candidate_recurse(&self, node: &Node, mut target: Decimal, mut path: Vec<usize>) -> Option<(u32, Decimal, Vec<usize>)> {
match &node.content {
NodeContent::Leaf(bitmap) => {
if bitmap.is_empty() { return None; }
let mut rng = rand::thread_rng();
let rand_index = rng.gen_range(0..bitmap.len() as u32);
let selected_id = bitmap.select(rand_index).unwrap();
let weight = node.accumulated_value / Decimal::from(node.content_count);
Some((selected_id, weight, path))
}
NodeContent::Internal(children) => {
for (i, child) in children.iter().enumerate() {
if child.accumulated_value.is_zero() { continue; }
if target < child.accumulated_value {
path.push(i);
return self.find_candidate_recurse(child, target, path);
}
target -= child.accumulated_value;
}
None }
}
}
fn update_and_remove_recurse(node: &mut Node, path: &[usize], id_to_remove: u32, weight: Decimal) {
node.content_count -= 1;
node.accumulated_value -= weight;
let Some(&index) = path.first() else {
if let NodeContent::Leaf(bitmap) = &mut node.content {
bitmap.remove(id_to_remove);
}
return;
};
if let NodeContent::Internal(children) = &mut node.content {
if let Some(child) = children.get_mut(index) {
Self::update_and_remove_recurse(child, &path[1..], id_to_remove, weight);
}
}
}
fn select_recurse(
node: &mut Node,
mut target: Decimal,
mut path: Vec<usize>,
) -> (u32, Decimal, Vec<usize>) {
match &mut node.content {
NodeContent::Leaf(bitmap) => {
let mut rng = rand::thread_rng();
let bitmap_len = bitmap.len() as u32;
let rand_index = rng.gen_range(0..bitmap_len);
let selected_id = bitmap.select(rand_index).unwrap();
let weight = node.accumulated_value / Decimal::from(node.content_count);
(selected_id, weight, path)
}
NodeContent::Internal(children) => {
for (i, child) in children.iter_mut().enumerate() {
if child.accumulated_value.is_zero() { continue; }
if target < child.accumulated_value {
path.push(i);
return Self::select_recurse(child, target, path);
}
target -= child.accumulated_value;
}
panic!("Selection logic failed: target exceeded total value of children.");
}
}
}
fn update_values_post_removal(&mut self, path: &[usize], weight: Decimal) {
let mut current_node = &mut self.root;
current_node.content_count -= 1;
current_node.accumulated_value -= weight;
for &index in path {
if let NodeContent::Internal(children) = &mut current_node.content {
current_node = &mut children[index];
current_node.content_count -= 1;
current_node.accumulated_value -= weight;
} else {
return;
}
}
}
pub fn count(&self) -> u32 {
self.root.content_count
}
pub fn total_weight(&self) -> Decimal {
self.root.accumulated_value
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_wallenius_distribution_is_correct() {
const ITEMS_PER_GROUP: u32 = 1000;
const TOTAL_ITEMS: u32 = ITEMS_PER_GROUP * 2;
const NUM_DRAWS: u32 = TOTAL_ITEMS / 2;
let low_risk_weight = dec!(0.1); let high_risk_weight = dec!(0.2);
const NUM_SIMULATIONS: u32 = 100;
let mut total_high_risk_selected = 0;
for _ in 0..NUM_SIMULATIONS {
let mut index = DigitBinIndex::with_precision(3);
for i in 0..ITEMS_PER_GROUP { index.add(i, low_risk_weight); }
for i in ITEMS_PER_GROUP..TOTAL_ITEMS { index.add(i, high_risk_weight); }
let mut high_risk_in_this_run = 0;
for _ in 0..NUM_DRAWS {
if let Some((selected_id, _)) = index.select_and_remove() {
if selected_id >= ITEMS_PER_GROUP {
high_risk_in_this_run += 1;
}
}
}
total_high_risk_selected += high_risk_in_this_run;
}
let avg_high_risk = total_high_risk_selected as f64 / NUM_SIMULATIONS as f64;
let uniform_mean = NUM_DRAWS as f64 * 0.5;
let fishers_mean = NUM_DRAWS as f64 * (2.0 / 3.0);
assert!(
avg_high_risk > uniform_mean,
"Test failed: Result {:.2} was not biased towards higher weights (uniform mean is {:.2})",
avg_high_risk, uniform_mean
);
assert!(
avg_high_risk < fishers_mean,
"Test failed: Result {:.2} showed too much bias. It should be less than the Fisher's mean of {:.2} due to the Wallenius effect.",
avg_high_risk, fishers_mean
);
println!(
"Distribution test passed: Got an average of {:.2} high-risk selections.",
avg_high_risk
);
println!(
"This correctly lies between the uniform mean ({:.2}) and the Fisher's mean ({:.2}), confirming the Wallenius' distribution behavior.",
uniform_mean, fishers_mean
);
}
#[test]
fn test_fisher_distribution_is_correct() {
const ITEMS_PER_GROUP: u32 = 1000;
const TOTAL_ITEMS: u32 = ITEMS_PER_GROUP * 2;
const NUM_DRAWS: u32 = TOTAL_ITEMS / 2;
let low_risk_weight = dec!(0.1); let high_risk_weight = dec!(0.2);
const NUM_SIMULATIONS: u32 = 100;
let mut total_high_risk_selected = 0;
for _ in 0..NUM_SIMULATIONS {
let mut index = DigitBinIndex::with_precision(3);
for i in 0..ITEMS_PER_GROUP { index.add(i, low_risk_weight); }
for i in ITEMS_PER_GROUP..TOTAL_ITEMS { index.add(i, high_risk_weight); }
if let Some(selected_ids) = index.select_many_and_remove(NUM_DRAWS) {
let high_risk_in_this_run = selected_ids.iter().filter(|&&id| id >= ITEMS_PER_GROUP).count();
total_high_risk_selected += high_risk_in_this_run as u32;
}
}
let avg_high_risk = total_high_risk_selected as f64 / NUM_SIMULATIONS as f64;
let fishers_mean = NUM_DRAWS as f64 * (2.0 / 3.0);
let tolerance = fishers_mean * 0.10;
assert!(
(avg_high_risk - fishers_mean).abs() < tolerance,
"Fisher's test failed: Result {:.2} was not close to the expected mean of {:.2}",
avg_high_risk, fishers_mean
);
println!(
"Fisher's test passed: Got avg {:.2} high-risk selections (expected ~{:.2}).",
avg_high_risk, fishers_mean
);
}
}