#[derive(Debug, Clone, Default)]
pub struct PriorityAccumulator {
scores: Vec<f32>,
order: Vec<usize>,
}
impl PriorityAccumulator {
pub fn new(entities: usize) -> Self {
Self {
scores: vec![0.0; entities],
order: Vec::with_capacity(entities),
}
}
pub fn resize(&mut self, entities: usize) {
self.scores.resize(entities, 0.0);
}
pub fn len(&self) -> usize {
self.scores.len()
}
pub fn is_empty(&self) -> bool {
self.scores.is_empty()
}
pub fn bump(&mut self, index: usize, priority: f32) {
if index >= self.scores.len() {
self.scores.resize(index + 1, 0.0);
}
self.scores[index] += priority;
}
pub fn forget(&mut self, index: usize) {
if let Some(score) = self.scores.get_mut(index) {
*score = 0.0;
}
}
pub fn score(&self, index: usize) -> f32 {
self.scores.get(index).copied().unwrap_or(0.0)
}
pub fn clear(&mut self) {
self.scores.fill(0.0);
}
pub fn order(&mut self, out: &mut Vec<usize>) {
out.clear();
out.extend((0..self.scores.len()).filter(|&i| self.scores[i] > 0.0));
out.sort_by(|&a, &b| self.scores[b].total_cmp(&self.scores[a]).then(a.cmp(&b)));
}
pub fn sent(&mut self, indices: &[usize]) {
for &index in indices {
if let Some(score) = self.scores.get_mut(index) {
*score = 0.0;
}
}
}
pub fn fill(&mut self, budget: usize, cost: impl Fn(usize) -> usize, out: &mut Vec<usize>) {
out.clear();
self.order.clear();
self.order.extend((0..self.scores.len()).filter(|&i| self.scores[i] > 0.0));
self
.order
.sort_by(|&a, &b| self.scores[b].total_cmp(&self.scores[a]).then(a.cmp(&b)));
let mut left = budget;
for &index in &self.order {
let price = cost(index);
if price > left {
continue;
}
left -= price;
self.scores[index] = 0.0;
out.push(index);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn flat(_: usize) -> usize {
10
}
#[test]
fn the_highest_go_first() {
let mut p = PriorityAccumulator::new(4);
p.bump(0, 1.0);
p.bump(1, 4.0);
p.bump(2, 2.0);
p.bump(3, 3.0);
let mut out = Vec::new();
p.fill(20, flat, &mut out);
assert_eq!(out, [1, 3]);
}
#[test]
fn what_did_not_fit_stays_hot_and_wins_later() {
let mut p = PriorityAccumulator::new(2);
let mut out = Vec::new();
let mut sent = [0u32; 2];
for _ in 0..10 {
p.bump(0, 1.0);
p.bump(1, 1.0);
p.fill(10, flat, &mut out);
assert_eq!(out.len(), 1);
sent[out[0]] += 1;
}
assert_eq!(sent, [5, 5], "neither starved");
}
#[test]
fn a_skipped_entity_keeps_its_score_and_a_sent_one_restarts() {
let mut p = PriorityAccumulator::new(2);
p.bump(0, 5.0);
p.bump(1, 1.0);
let mut out = Vec::new();
p.fill(10, flat, &mut out);
assert_eq!(out, [0]);
assert_eq!(p.score(0), 0.0);
assert_eq!(p.score(1), 1.0);
}
#[test]
fn a_budget_is_a_ceiling_not_a_target() {
let mut p = PriorityAccumulator::new(3);
for i in 0..3 {
p.bump(i, (3 - i) as f32);
}
let mut out = Vec::new();
p.fill(25, flat, &mut out);
assert_eq!(out.len(), 2, "two fit in 25, the third does not");
}
#[test]
fn one_oversized_entity_does_not_empty_the_packet() {
let mut p = PriorityAccumulator::new(3);
p.bump(0, 9.0);
p.bump(1, 2.0);
p.bump(2, 1.0);
let mut out = Vec::new();
p.fill(20, |i| if i == 0 { 100 } else { 10 }, &mut out);
assert_eq!(out, [1, 2]);
assert_eq!(p.score(0), 9.0, "and it is still owed a turn");
}
#[test]
fn zero_and_negative_scores_are_never_chosen() {
let mut p = PriorityAccumulator::new(3);
p.bump(1, -5.0);
let mut out = Vec::new();
p.fill(1000, flat, &mut out);
assert!(out.is_empty(), "nothing has earned a slot yet");
}
#[test]
fn ties_break_by_index_so_two_machines_agree() {
let mut a = PriorityAccumulator::new(4);
let mut b = PriorityAccumulator::new(4);
for i in 0..4 {
a.bump(i, 1.0);
b.bump(i, 1.0);
}
let (mut x, mut y) = (Vec::new(), Vec::new());
a.fill(20, flat, &mut x);
b.fill(20, flat, &mut y);
assert_eq!(x, y);
assert_eq!(x, [0, 1]);
}
#[test]
fn order_ranks_without_clearing_and_sent_is_what_clears() {
let mut p = PriorityAccumulator::new(4);
for i in 0..4 {
p.bump(i, (4 - i) as f32);
}
let mut order = Vec::new();
p.order(&mut order);
assert_eq!(order, [0, 1, 2, 3], "hottest first");
assert_eq!(p.score(0), 4.0, "ranking is not sending");
p.sent(&order[..2]);
assert_eq!(p.score(0), 0.0);
assert_eq!(p.score(1), 0.0);
assert_eq!(p.score(2), 2.0);
assert_eq!(p.score(3), 1.0);
}
#[test]
fn nothing_starves_when_the_caller_packs_until_full() {
let mut p = PriorityAccumulator::new(6);
let mut seen = vec![0u32; 6];
let mut order = Vec::new();
for _ in 0..60 {
for i in 0..6 {
p.bump(i, 1.0);
}
p.order(&mut order);
let fitted: Vec<usize> = order.iter().copied().take(2).collect();
p.sent(&fitted);
for i in fitted {
seen[i] += 1;
}
}
assert!(seen.iter().all(|&n| n >= 15), "{seen:?}");
}
#[test]
fn bumping_past_the_end_grows_rather_than_panics() {
let mut p = PriorityAccumulator::new(1);
p.bump(7, 1.0);
assert_eq!(p.len(), 8);
assert_eq!(p.score(7), 1.0);
}
#[test]
fn forgetting_drops_a_hoard() {
let mut p = PriorityAccumulator::new(2);
p.bump(0, 100.0);
p.forget(0);
let mut out = Vec::new();
p.fill(1000, flat, &mut out);
assert!(out.is_empty());
}
}