use wyrand::WyRand;
use rand::{distr::{Distribution, Uniform}, Rng, SeedableRng};
use roaring::{RoaringBitmap, RoaringTreemap};
const DEFAULT_PRECISION: u8 = 3;
const MAX_PRECISION: usize = 9;
pub trait DigitBin: Clone + Default {
fn insert(&mut self, id: u64);
fn remove(&mut self, id: u64) -> bool;
fn len(&self) -> usize;
fn is_empty(&self) -> bool;
fn get_random(&self, rng: &mut impl rand::Rng) -> Option<u64>;
fn get_random_and_remove(&mut self, rng: &mut impl rand::Rng) -> Option<u64>;
}
impl DigitBin for Vec<u32> {
fn insert(&mut self, id: u64) { self.push(id as u32); }
fn remove(&mut self, id: u64) -> bool {
if let Some(pos) = self.iter().position(|&x| x == id as u32) {
self.swap_remove(pos);
true
} else {
false
}
}
fn len(&self) -> usize { self.len() }
fn is_empty(&self) -> bool { self.is_empty() }
fn get_random(&self, rng: &mut impl rand::Rng) -> Option<u64> {
if self.is_empty() { None } else { Some(self[rng.random_range(0..self.len())] as u64) }
}
fn get_random_and_remove(&mut self, rng: &mut impl rand::Rng) -> Option<u64> {
if self.is_empty() { None } else {
let pos = rng.random_range(0..self.len());
Some(self.swap_remove(pos) as u64)
}
}
}
impl DigitBin for RoaringBitmap {
fn insert(&mut self, id: u64) { self.insert(id as u32); }
fn remove(&mut self, id: u64) -> bool { self.remove(id as u32) }
fn len(&self) -> usize { self.len() as usize }
fn is_empty(&self) -> bool { self.is_empty() }
fn get_random(&self, rng: &mut impl rand::Rng) -> Option<u64> {
if self.is_empty() { None } else {
let idx = rng.random_range(0..self.len() as u32);
self.select(idx).map(|v| v as u64)
}
}
fn get_random_and_remove(&mut self, rng: &mut impl rand::Rng) -> Option<u64> {
if self.is_empty() { None } else {
let idx = rng.random_range(0..self.len() as u32);
let selected = self.select(idx);
self.remove(selected.unwrap());
selected.map(|v| v as u64)
}
}
}
impl DigitBin for RoaringTreemap {
fn insert(&mut self, id: u64) { self.insert(id); }
fn remove(&mut self, id: u64) -> bool { self.remove(id) }
fn len(&self) -> usize { self.len() as usize }
fn is_empty(&self) -> bool { self.is_empty() }
fn get_random(&self, rng: &mut impl rand::Rng) -> Option<u64> {
if self.is_empty() { None } else {
let idx = rng.random_range(0..self.len() as u64);
self.select(idx)
}
}
fn get_random_and_remove(&mut self, rng: &mut impl rand::Rng) -> Option<u64> {
if self.is_empty() { None } else {
let idx = rng.random_range(0..self.len());
let selected = self.select(idx);
self.remove(selected.unwrap());
selected
}
}
}
fn new_children_array<B: DigitBin>() -> Box<[Option<Node<B>>; 10]> {
let data: [Option<Node<B>>; 10] = Default::default();
Box::new(data)
}
#[derive(Debug, Clone)]
pub enum NodeContent<B: DigitBin> {
DigitIndex(Box<[Option<Node<B>>; 10]>),
Bin(B),
}
#[derive(Debug, Clone)]
pub struct Node<B: DigitBin> {
pub content: NodeContent<B>,
pub accumulated_value: u64,
pub content_count: u64,
}
impl<B: DigitBin> Node<B> {
fn new_internal() -> Self {
Self {
content: NodeContent::DigitIndex(new_children_array()),
accumulated_value: 0u64,
content_count: 0,
}
}
}
#[derive(Debug, Clone)]
pub enum DigitBinIndex {
Small(DigitBinIndexGeneric<Vec<u32>>),
Medium(DigitBinIndexGeneric<RoaringBitmap>),
Large(DigitBinIndexGeneric<RoaringTreemap>),
}
impl DigitBinIndex {
pub fn with_precision_and_capacity(precision: u8, capacity: u64) -> Self {
let max_bins = 10u64.pow(precision as u32);
if capacity / max_bins > 1_000_000_000 {
DigitBinIndex::Large(DigitBinIndexGeneric::<RoaringTreemap>::with_precision(precision))
}
else if capacity / max_bins > 1_000 {
DigitBinIndex::Medium(DigitBinIndexGeneric::<RoaringBitmap>::with_precision(precision))
} else {
DigitBinIndex::Small(DigitBinIndexGeneric::<Vec<u32>>::with_precision(precision))
}
}
pub fn new() -> Self {
DigitBinIndex::Small(DigitBinIndexGeneric::<Vec<u32>>::new())
}
pub fn with_precision(precision: u8) -> Self {
DigitBinIndex::Small(DigitBinIndexGeneric::<Vec<u32>>::with_precision(precision))
}
pub fn add(&mut self, id: u64, weight: f64) {
match self {
DigitBinIndex::Small(index) => index.add(id, weight),
DigitBinIndex::Medium(index) => index.add(id, weight),
DigitBinIndex::Large(index) => index.add(id, weight),
}
}
pub fn add_many(&mut self, items: &[(u64, f64)]) {
match self {
DigitBinIndex::Small(index) => index.add_many(items),
DigitBinIndex::Medium(index) => index.add_many(items),
DigitBinIndex::Large(index) => index.add_many(items),
}
}
pub fn remove(&mut self, id: u64, weight: f64) -> bool {
match self {
DigitBinIndex::Small(index) => index.remove(id, weight),
DigitBinIndex::Medium(index) => index.remove(id, weight),
DigitBinIndex::Large(index) => index.remove(id, weight),
}
}
pub fn remove_many(&mut self, items: &[(u64, f64)]) -> bool {
match self {
DigitBinIndex::Small(index) => index.remove_many(items),
DigitBinIndex::Medium(index) => index.remove_many(items),
DigitBinIndex::Large(index) => index.remove_many(items),
}
}
pub fn select(&mut self) -> Option<(u64, f64)> {
match self {
DigitBinIndex::Small(index) => index.select(),
DigitBinIndex::Medium(index) => index.select(),
DigitBinIndex::Large(index) => index.select(),
}
}
pub fn select_and_remove(&mut self) -> Option<(u64, f64)> {
match self {
DigitBinIndex::Small(index) => index.select_and_remove(),
DigitBinIndex::Medium(index) => index.select_and_remove(),
DigitBinIndex::Large(index) => index.select_and_remove(),
}
}
pub fn select_many(&mut self, num_to_draw: u64) -> Option<Vec<(u64, f64)>> {
match self {
DigitBinIndex::Small(index) => index.select_many(num_to_draw),
DigitBinIndex::Medium(index) => index.select_many(num_to_draw),
DigitBinIndex::Large(index) => index.select_many(num_to_draw),
}
}
pub fn select_many_and_remove(&mut self, num_to_draw: u64) -> Option<Vec<(u64, f64)>> {
match self {
DigitBinIndex::Small(index) => index.select_many_and_remove(num_to_draw),
DigitBinIndex::Medium(index) => index.select_many_and_remove(num_to_draw),
DigitBinIndex::Large(index) => index.select_many_and_remove(num_to_draw),
}
}
pub fn count(&self) -> u64 {
match self {
DigitBinIndex::Small(index) => index.count(),
DigitBinIndex::Medium(index) => index.count(),
DigitBinIndex::Large(index) => index.count(),
}
}
pub fn total_weight(&self) -> f64 {
match self {
DigitBinIndex::Small(index) => index.total_weight(),
DigitBinIndex::Medium(index) => index.total_weight(),
DigitBinIndex::Large(index) => index.total_weight(),
}
}
pub fn print_stats(&self) {
println!("DigitBinIndex Statistics:");
println!("=========================");
match self {
DigitBinIndex::Small(idx) => {
println!("- Index Type: Small (Vec<u32>)");
idx.print_stats_generic();
},
DigitBinIndex::Medium(idx) => {
println!("- Index Type: Medium (RoaringBitmap)");
idx.print_stats_generic();
},
DigitBinIndex::Large(idx) => {
println!("- Index Type: Large (RoaringTreemap)");
idx.print_stats_generic();
},
}
}
pub fn precision(&self) -> u8 {
match self {
DigitBinIndex::Small(idx) => idx.precision,
DigitBinIndex::Medium(idx) => idx.precision,
DigitBinIndex::Large(idx) => idx.precision,
}
}
}
#[derive(Debug, Clone)]
pub struct DigitBinIndexGeneric<B: DigitBin> {
pub root: Node<B>,
pub precision: u8,
scale: f64,
}
impl<B: DigitBin> Default for DigitBinIndexGeneric<B> {
fn default() -> Self {
Self::new()
}
}
impl<B: DigitBin> DigitBinIndexGeneric<B> {
#[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.");
assert!(precision <= MAX_PRECISION as u8, "Precision cannot be larger than {}.", MAX_PRECISION);
Self {
root: Node::new_internal(),
precision,
scale: 10f64.powi(precision as i32),
}
}
fn weight_to_digits(&self, weight: f64, digits: &mut [u8; MAX_PRECISION]) -> Option<u64> {
if weight <= 0.0 {
return None;
}
let scaled_f = weight * self.scale;
let scaled = scaled_f.round() as u64;
if scaled == 0 {
return None;
}
let mut temp = scaled;
for i in (0..self.precision as usize).rev() {
digits[i] = (temp % 10) as u8;
temp /= 10;
}
if temp != 0 {
return None;
}
Some(scaled)
}
pub fn add(&mut self, individual_id: u64, weight: f64) {
let mut digits = [0u8; MAX_PRECISION];
if let Some(scaled) = self.weight_to_digits(weight, &mut digits) {
Self::add_recurse(&mut self.root, individual_id, scaled, &digits, 1, self.precision)
}
}
fn add_recurse(
node: &mut Node<B>,
individual_id: u64,
scaled: u64, digits: &[u8; MAX_PRECISION],
current_depth: u8,
max_depth: u8,
) {
node.content_count += 1;
node.accumulated_value += scaled;
if current_depth > max_depth {
if let NodeContent::DigitIndex(_) = &node.content {
node.content = NodeContent::Bin(B::default());
}
if let NodeContent::Bin(bin) = &mut node.content {
bin.insert(individual_id);
}
return;
}
let digit = digits[current_depth as usize - 1] as usize;
if let NodeContent::DigitIndex(children) = &mut node.content {
let child_node = children[digit].get_or_insert_with(Node::new_internal);
Self::add_recurse(child_node, individual_id, scaled, digits, current_depth + 1, max_depth);
}
}
pub fn add_many(&mut self, items: &[(u64, f64)]) {
if items.is_empty() {
return;
}
let mut digits = [0u8; MAX_PRECISION];
for &(id, weight) in items {
if let Some(scaled) = self.weight_to_digits(weight, &mut digits) {
Self::add_recurse(&mut self.root, id, scaled, &digits, 1, self.precision)
}
}
}
pub fn remove(&mut self, individual_id: u64, weight: f64) -> bool{
let mut digits = [0u8; MAX_PRECISION];
if let Some(scaled) = self.weight_to_digits(weight, &mut digits) {
return Self::remove_recurse(&mut self.root, individual_id, scaled, &digits, 1, self.precision);
}
false
}
fn remove_recurse(
node: &mut Node<B>,
individual_id: u64,
scaled: u64,
digits: &[u8; MAX_PRECISION],
current_depth: u8,
max_depth: u8,
) -> bool {
if current_depth > max_depth {
if let NodeContent::Bin(bin) = &mut node.content {
let orig_len = bin.len();
bin.remove(individual_id);
if bin.len() < orig_len {
node.content_count -= 1;
node.accumulated_value -= scaled;
return true;
}
}
return false;
}
let digit = digits[current_depth as usize - 1] as usize;
if let NodeContent::DigitIndex(children) = &mut node.content {
if let Some(child_node) = children[digit].as_mut() {
if Self::remove_recurse(child_node, individual_id, scaled, digits, current_depth + 1, max_depth) {
node.content_count -= 1;
node.accumulated_value -= scaled;
return true;
}
}
}
false
}
pub fn remove_many(&mut self, items: &[(u64, f64)]) -> bool {
if items.is_empty() {
return false;
}
let mut digits = [0u8; MAX_PRECISION];
let mut success = true;
for &(id, weight) in items {
if let Some(scaled) = self.weight_to_digits(weight, &mut digits) {
success &= Self::remove_recurse(&mut self.root, id, scaled, &digits, 1, self.precision)
} else {
success &= false;
}
}
success
}
pub fn select(&mut self) -> Option<(u64, f64)> {
self.select_and_optionally_remove(false)
}
pub fn select_many(&mut self, num_to_draw: u64) -> Option<Vec<(u64, f64)>> {
self.select_many_and_optionally_remove(num_to_draw, false)
}
pub fn select_and_remove(&mut self) -> Option<(u64, f64)> {
self.select_and_optionally_remove(true)
}
pub fn select_and_optionally_remove(&mut self, with_removal: bool) -> Option<(u64, f64)> {
if self.root.content_count == 0 {
return None;
}
let mut rng = WyRand::from_os_rng();
let random_target = rng.random_range(0u64..self.root.accumulated_value);
Self::select_and_optionally_remove_recurse(&mut self.root, random_target, 1, self.precision, &mut rng, with_removal, self.scale)
}
fn select_and_optionally_remove_recurse(
node: &mut Node<B>,
target: u64,
current_depth: u8,
max_depth: u8,
rng: &mut WyRand,
with_removal: bool,
scale: f64,
) -> Option<(u64, f64)> {
if current_depth > max_depth {
if let NodeContent::Bin(bin) = &mut node.content {
if bin.is_empty() {
return None;
}
let scaled_weight = node.accumulated_value / node.content_count as u64;
let weight = scaled_weight as f64 / scale;
let selected_id = if with_removal {
bin.get_random_and_remove(rng)?
} else {
bin.get_random(rng)?
};
if with_removal {
node.content_count -= 1;
node.accumulated_value -= scaled_weight;
}
return Some((selected_id, weight));
}
return None;
}
if let NodeContent::DigitIndex(children) = &mut node.content {
let mut cum: u64 = 0;
for child_option in children.iter_mut() {
if let Some(child) = child_option.as_mut() {
if child.accumulated_value == 0 {
continue;
}
if target < cum + child.accumulated_value {
if let Some((selected_id, weight)) = Self::select_and_optionally_remove_recurse(
child,
target - cum,
current_depth + 1,
max_depth,
rng,
with_removal,
scale,
) {
if with_removal {
node.content_count -= 1;
node.accumulated_value -= (weight * scale).round() as u64;
}
return Some((selected_id, weight));
}
return None;
}
cum += child.accumulated_value;
}
}
}
None
}
pub fn select_many_and_remove(&mut self, num_to_draw: u64) -> Option<Vec<(u64, f64)>> {
self.select_many_and_optionally_remove(num_to_draw, true)
}
pub fn select_many_and_optionally_remove(&mut self, num_to_draw: u64, with_removal: bool) -> Option<Vec<(u64, f64)>> {
if num_to_draw > self.count() || num_to_draw == 0 {
return if num_to_draw == 0 { Some(Vec::new()) } else { None };
}
let mut rng = WyRand::from_os_rng();
let mut selected: Vec<(u64, f64)> = Vec::with_capacity(num_to_draw as usize);
let total_accum = self.root.accumulated_value;
let uniform = Uniform::new(0u64, total_accum).expect("Valid range for Uniform");
let passed_targets: Vec<u64> = uniform
.sample_iter(&mut rng)
.take(num_to_draw as usize)
.collect();
Self::select_many_and_optionally_remove_recurse(
&mut self.root,
total_accum,
&mut selected,
&mut rng,
1,
self.precision,
with_removal,
passed_targets,
self.scale,
);
if selected.len() == num_to_draw as usize {
Some(selected)
} else {
None }
}
fn select_many_and_optionally_remove_recurse(
node: &mut Node<B>,
subtree_total: u64,
selected: &mut Vec<(u64, f64)>,
rng: &mut WyRand,
current_depth: u8,
precision: u8,
with_removal: bool,
passed_targets: Vec<u64>,
scale: f64,
) {
let original_target_count = passed_targets.len() as u64;
if original_target_count == 0 {
return;
}
if current_depth > precision {
if let NodeContent::Bin(bin) = &mut node.content {
let bin_scaled = if node.content_count > 0 {
node.accumulated_value / node.content_count as u64
} else {
0u64
};
let bin_weight = bin_scaled as f64 / scale;
let to_select = original_target_count.min(node.content_count);
let mut picked = 0u64;
while picked < to_select && !bin.is_empty() {
let id = if with_removal {
bin.get_random_and_remove(rng).unwrap()
} else {
bin.get_random(rng).unwrap()
};
selected.push((id, bin_weight));
picked += 1;
}
if with_removal {
node.content_count -= picked;
node.accumulated_value -= bin_scaled * picked as u64;
}
}
return;
}
if let NodeContent::DigitIndex(children) = &mut node.content {
let mut child_assigned = [0u64; 10];
let mut child_rel_targets: [Vec<u64>; 10] = Default::default();
let mut assigned = 0u64;
for &target in &passed_targets {
let mut cum: u64 = 0;
let mut chosen_idx = None;
for (i, child_option) in children.iter().enumerate() {
if let Some(child) = child_option {
if child.accumulated_value == 0 {
continue;
}
if target < cum + child.accumulated_value {
if child_assigned[i] + 1 <= child.content_count {
chosen_idx = Some(i);
}
break;
}
cum += child.accumulated_value;
}
}
if let Some(idx) = chosen_idx {
child_assigned[idx] += 1;
let start_of_child_range: u64 = children[..idx].iter().filter_map(|c| c.as_ref()).map(|c| c.accumulated_value).sum();
let rel_target = target - start_of_child_range;
child_rel_targets[idx].push(rel_target);
assigned += 1;
}
}
let remaining = original_target_count - assigned;
let mut additional_assigned = 0u64;
while additional_assigned < remaining {
let target = rng.random_range(0u64..subtree_total);
let mut cum: u64 = 0;
let mut chosen_idx = None;
for (i, child_option) in children.iter().enumerate() {
if let Some(child) = child_option {
if child.accumulated_value == 0 {
continue;
}
if target < cum + child.accumulated_value {
if child_assigned[i] + 1 <= child.content_count {
chosen_idx = Some(i);
}
break;
}
cum += child.accumulated_value;
}
}
if let Some(idx) = chosen_idx {
child_assigned[idx] += 1;
let start_of_child_range: u64 = children[..idx].iter().filter_map(|c| c.as_ref()).map(|c| c.accumulated_value).sum();
let rel_target = target - start_of_child_range;
child_rel_targets[idx].push(rel_target);
additional_assigned += 1;
}
}
let child_accums: [u64; 10] = std::array::from_fn(|i| {
children[i].as_ref().map_or(0, |c| c.accumulated_value)
});
for (i, child_option) in children.iter_mut().enumerate() {
let assign_count = child_assigned[i];
if assign_count > 0 {
if let Some(child) = child_option {
let rel_targets = std::mem::take(&mut child_rel_targets[i]);
Self::select_many_and_optionally_remove_recurse(
child,
child_accums[i],
selected,
rng,
current_depth + 1,
precision,
with_removal,
rel_targets,
scale,
);
}
}
}
if with_removal {
node.content_count = children.iter().filter_map(|c| c.as_ref()).map(|c| c.content_count).sum();
node.accumulated_value = children.iter().filter_map(|c| c.as_ref()).map(|c| c.accumulated_value).sum();
}
}
}
pub fn count(&self) -> u64 {
self.root.content_count
}
pub fn total_weight(&self) -> f64 {
self.root.accumulated_value as f64 / self.scale
}
pub fn print_stats_generic(&self) {
struct Stats {
node_count: usize,
non_empty_node_count: usize,
internal_node_count: usize, child_slots_used: usize, bin_count: usize,
empty_bin_count: usize,
total_bin_items: u64,
min_weight: Option<f64>,
max_weight: Option<f64>,
bin_sizes: Vec<usize>,
mem_nodes: usize,
mem_bins: usize,
}
fn traverse<B: DigitBin>(
node: &Node<B>,
stats: &mut Stats,
scale: f64,
) {
stats.node_count += 1;
stats.mem_nodes += std::mem::size_of::<Node<B>>();
if node.content_count > 0 {
stats.non_empty_node_count += 1;
}
match &node.content {
NodeContent::DigitIndex(children) => {
stats.internal_node_count += 1;
let used_children = children.iter().filter(|c| c.is_some()).count();
stats.child_slots_used += used_children;
stats.mem_nodes += std::mem::size_of::<[Option<Node<B>>; 10]>();
for child_option in children.iter() {
if let Some(child) = child_option {
traverse(child, stats, scale);
}
}
}
NodeContent::Bin(bin) => {
stats.bin_count += 1;
let bin_size = bin.len();
stats.bin_sizes.push(bin_size);
stats.total_bin_items += bin_size as u64;
stats.mem_bins += bin_size * std::mem::size_of::<u32>();
if bin_size == 0 {
stats.empty_bin_count += 1;
} else {
let scaled_weight = node.accumulated_value / node.content_count;
let weight = scaled_weight as f64 / scale;
stats.min_weight = Some(stats.min_weight.map_or(weight, |min| min.min(weight)));
stats.max_weight = Some(stats.max_weight.map_or(weight, |max| max.max(weight)));
}
}
}
}
let mut stats = Stats {
node_count: 0,
non_empty_node_count: 0,
internal_node_count: 0, child_slots_used: 0, bin_count: 0,
empty_bin_count: 0,
total_bin_items: 0,
min_weight: None,
max_weight: None,
bin_sizes: Vec::new(),
mem_nodes: 0,
mem_bins: 0,
};
traverse(&self.root, &mut stats, self.scale);
let fill_ratio = if stats.node_count > 0 {
stats.non_empty_node_count as f64 / stats.node_count as f64 * 100.0
} else { 0.0 };
let avg_branching_factor = if stats.internal_node_count > 0 {
stats.child_slots_used as f64 / stats.internal_node_count as f64
} else { 0.0 };
let avg_bin_size = if stats.bin_count > 0 {
stats.total_bin_items as f64 / stats.bin_count as f64
} else { 0.0 };
let std_dev_bin_size = if stats.bin_count > 1 {
let variance = stats.bin_sizes.iter()
.map(|&size| (size as f64 - avg_bin_size).powi(2)) .sum::<f64>() / (stats.bin_count - 1) as f64;
variance.sqrt()
} else { 0.0 };
let (q1_bin_size, median_bin_size, q3_bin_size) = if !stats.bin_sizes.is_empty() {
let mut sorted_sizes = stats.bin_sizes.clone();
sorted_sizes.sort_unstable();
let q1 = sorted_sizes.get(sorted_sizes.len() / 4).cloned().unwrap_or(0);
let median = sorted_sizes.get(sorted_sizes.len() / 2).cloned().unwrap_or(0);
let q3 = sorted_sizes.get(sorted_sizes.len() * 3 / 4).cloned().unwrap_or(0);
(q1, median, q3)
} else {
(0, 0, 0)
};
let total_mem_mb = (stats.mem_nodes + stats.mem_bins) as f64 / (1024.0 * 1024.0);
let nodes_mem_mb = stats.mem_nodes as f64 / (1024.0 * 1024.0);
let bins_mem_mb = stats.mem_bins as f64 / (1024.0 * 1024.0);
let avg_weight = if self.count() > 0 {
self.total_weight() / self.count() as f64
} else { 0.0 };
println!("\n[Tree Structure]");
println!("- Total Nodes Created: {}", stats.node_count);
println!("- Internal Nodes: {}", stats.internal_node_count); println!("- Avg Branching Factor: {:.2} / 10", avg_branching_factor); println!("- Tree Fill Ratio: {:.2}%", fill_ratio);
println!("- Max Depth: {}", self.precision);
println!("\n[Memory (Estimated)]");
println!("- Tree Structure: {:.2} MB", nodes_mem_mb);
println!("- Leaf Bins: {:.2} MB", bins_mem_mb);
println!("- Total Estimated: {:.2} MB", total_mem_mb);
println!("\n[Items & Bins]");
println!("- Total Items: {}", stats.total_bin_items);
println!("- Total Bins (Leaves): {}", stats.bin_count);
println!("- Empty Bins: {}", stats.empty_bin_count);
println!("- Avg Items per Bin: {:.2}", avg_bin_size);
println!("- Std Dev of Bin Size: {:.2}", std_dev_bin_size);
println!("- Bin Size (min/max): {} / {}", stats.bin_sizes.iter().min().map_or(0, |v| *v), stats.bin_sizes.iter().max().map_or(0, |v| *v));
println!("- Bin Size (Q1/Med/Q3): {} / {} / {}", q1_bin_size, median_bin_size, q3_bin_size);
println!("\n[Weights]");
println!("- Smallest Weight: {}", stats.min_weight.map_or("-".to_string(), |v| format!("{:.prec$}", v, prec = self.precision as usize)));
println!("- Largest Weight: {}", stats.max_weight.map_or("-".to_string(), |v| format!("{:.prec$}", v, prec = self.precision as usize)));
println!("- Average Weight: {:.prec$}", avg_weight, prec = self.precision as usize); }
}
#[cfg(feature = "python-bindings")]
mod python {
use super::*;
use pyo3::prelude::*;
#[pyclass(name = "DigitBinIndex")]
struct PyDigitBinIndex {
index: DigitBinIndex,
}
#[pymethods]
impl PyDigitBinIndex {
#[new]
fn new() -> Self {
PyDigitBinIndex {
index: DigitBinIndex::new(),
}
}
#[staticmethod]
fn with_precision(precision: u64) -> Self {
PyDigitBinIndex {
index: DigitBinIndex::with_precision(precision.try_into().unwrap()),
}
}
#[staticmethod]
fn with_precision_and_capacity(precision: u8, capacity: u64) -> Self {
PyDigitBinIndex {
index: DigitBinIndex::with_precision_and_capacity(precision, capacity),
}
}
fn add(&mut self, id: u64, weight: f64) {
self.index.add(id, weight)
}
fn add_many(&mut self, items: Vec<(u64, f64)>) {
self.index.add_many(&items);
}
fn remove(&mut self, id: u64, weight: f64) -> bool {
self.index.remove(id, weight)
}
fn remove_many(&mut self, items: Vec<(u64, f64)>) -> bool {
self.index.remove_many(&items)
}
fn select(&mut self) -> Option<(u64, f64)> {
self.index.select()
}
fn select_many(&mut self, n: u64) -> Option<Vec<(u64, f64)>> {
self.index.select_many(n)
}
fn select_and_remove(&mut self) -> Option<(u64, f64)> {
self.index.select_and_remove()
}
fn select_many_and_remove(&mut self, n: u64) -> Option<Vec<(u64, f64)>> {
self.index.select_many_and_remove(n)
}
fn total_weight(&self) -> f64 {
self.index.total_weight()
}
fn count(&self) -> u64 {
self.index.count()
}
}
#[pymodule]
fn digit_bin_index(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyDigitBinIndex>()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_select_and_remove() {
let mut index = DigitBinIndex::with_precision(3);
index.add(1, 0.085);
index.add(2, 0.205);
index.add(3, 0.346);
index.add(4, 0.364);
index.print_stats();
println!("Initial state: {} individuals, total weight = {}", index.count(), index.total_weight());
if let Some((id, weight)) = index.select_and_remove() {
println!("Selected ID: {} with weight: {}", id, weight);
}
assert!(
index.count() == 3,
"The count is now {} and not 3 as expected",
index.count()
);
println!("Intermediate state: {} individuals, total weight = {}", index.count(), index.total_weight());
if let Some(selection) = index.select_many_and_remove(2) {
println!("Selection: {:?}", selection);
}
assert!(
index.count() == 1,
"The count is now {} and not 1 as expected",
index.count()
);
println!("Final state: {} individuals, total weight = {}", index.count(), index.total_weight());
}
#[test]
fn test_wallenius_distribution_is_correct() {
const ITEMS_PER_GROUP: u64 = 1000;
const TOTAL_ITEMS: u64 = ITEMS_PER_GROUP * 2;
const NUM_DRAWS: u64 = TOTAL_ITEMS / 2;
let low_risk_weight = 0.1f64; let high_risk_weight = 0.2f64;
const NUM_SIMULATIONS: u32 = 100;
let mut total_high_risk_selected = 0;
for _ in 0..NUM_SIMULATIONS {
let mut index = DigitBinIndex::with_precision_and_capacity(3, TOTAL_ITEMS);
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: u64 = 1000;
const TOTAL_ITEMS: u64 = ITEMS_PER_GROUP * 2;
const NUM_DRAWS: u64 = TOTAL_ITEMS / 2;
let low_risk_weight = 0.1f64; let high_risk_weight = 0.2f64;
const NUM_SIMULATIONS: u32 = 100;
let mut total_high_risk_selected = 0;
for _ in 0..NUM_SIMULATIONS {
let mut index = DigitBinIndex::with_precision_and_capacity(3, TOTAL_ITEMS);
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.02;
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
);
}
}
#[cfg(test)]
#[test]
fn test_weight_to_digits() {
let index = DigitBinIndexGeneric::<Vec<u32>>::with_precision(3);
let mut digits = [0u8; MAX_PRECISION];
if let Some(scaled) = index.weight_to_digits(0.123, &mut digits) {
assert_eq!(scaled, 123);
assert_eq!(digits[0..3], [1, 2, 3]);
assert_eq!(digits[3..], [0; 6]); } else {
panic!("Expected Some for valid weight");
}
assert!(index.weight_to_digits(0.0, &mut digits).is_none());
assert!(index.weight_to_digits(-0.1, &mut digits).is_none());
assert!(index.weight_to_digits(0.0000001, &mut digits).is_none());
assert!(index.weight_to_digits(2.0, &mut digits).is_none()); }
#[cfg(test)]
#[test]
fn test_add_many() {
const CAPACITY: u64 = 1_000_000u64;
let mut index_one_at_a_time = DigitBinIndex::with_precision_and_capacity(3, CAPACITY);
let mut index_all_at_once = DigitBinIndex::with_precision_and_capacity(3, CAPACITY);
let mut population = Vec::with_capacity(CAPACITY as usize);
let mut rng = WyRand::from_os_rng();
for i in 0..CAPACITY {
let weight: f64 = rng.random_range(0.001..=0.999);
population.push((i, weight));
index_one_at_a_time.add(i, weight);
}
index_all_at_once.add_many(&population);
index_one_at_a_time.print_stats();
index_all_at_once.print_stats();
}
#[test]
fn test_add() {
let mut index = DigitBinIndex::new();
index.add(1, 0.5);
index.print_stats();
}