use crate::error::Error;
const TAU: f64 = 4.0 * f64::EPSILON;
#[derive(Debug, Clone)]
pub struct Selection {
pub hits: Vec<u64>,
pub effective_lambda: Vec<f64>,
pub sample_size: u64,
pub eps_sum: f64,
pub corrected_unit: Option<usize>,
pub adjusted_crossings: usize,
pub max_adjustment: f64,
}
pub fn pmr(lambda: &[f64], uniforms: &[f64], priority: &[usize]) -> Result<Selection, Error> {
let n_units = lambda.len();
if uniforms.len() != n_units {
return Err(Error::Input(format!(
"{} expected hits but {} uniforms; one draw per unit",
n_units,
uniforms.len()
)));
}
if priority.len() != n_units {
return Err(Error::Input(format!(
"{} expected hits but {} priorities",
n_units,
priority.len()
)));
}
if n_units == 0 {
return Err(Error::Input("an empty list cannot be sampled".into()));
}
for (i, &l) in lambda.iter().enumerate() {
if !l.is_finite() || l < 0.0 {
return Err(Error::Input(format!(
"expected hits must be finite and non-negative; unit {i} has {l}"
)));
}
}
for (i, &u) in uniforms.iter().enumerate() {
if !(0.0..1.0).contains(&u) {
return Err(Error::Input(format!(
"uniforms must lie in [0, 1); draw {i} is {u}"
)));
}
}
let mut sum = 0.0_f64;
let mut comp = 0.0_f64;
for &x in lambda {
let t = sum + x;
if sum.abs() >= x.abs() {
comp += (sum - t) + x;
} else {
comp += (x - t) + sum;
}
sum = t;
}
let n = (sum + comp).round_ties_even();
let eps_sum = (sum - n) + comp;
if eps_sum.abs() >= 1e-8 {
return Err(Error::Drift { eps: eps_sum });
}
let n = n as u64;
let k: Vec<f64> = lambda.iter().map(|l| l.floor()).collect();
let mut r: Vec<f64> = lambda.iter().zip(&k).map(|(l, ki)| l - ki).collect();
let mut corrected_unit = None;
if eps_sum != 0.0 {
let candidate = (0..n_units)
.filter(|&i| {
if eps_sum > 0.0 {
r[i] > eps_sum
} else {
r[i] > 0.0 && r[i] - eps_sum < 1.0
}
})
.min_by(|&a, &b| {
r[b].partial_cmp(&r[a])
.unwrap()
.then(priority[a].cmp(&priority[b]))
});
let j = candidate.ok_or(Error::NoAbsorber { eps: eps_sum })?;
r[j] -= eps_sum;
corrected_unit = Some(j);
}
let mut hits_cum_floor = vec![0_i64; n_units]; let mut remainder_after = vec![0.0_f64; n_units]; let mut taken = vec![0_i64; n_units]; let mut hits = vec![0_i64; n_units];
let mut frac = 0.0_f64;
let mut frac_comp = 0.0_f64;
let mut debt = 0.0_f64;
let mut adjusted_crossings = 0_usize;
for i in 0..n_units {
let (i_prev, f_prev, t_prev) = if i == 0 {
(0_i64, 0.0_f64, 0_i64)
} else {
(hits_cum_floor[i - 1], remainder_after[i - 1], taken[i - 1])
};
let repay = debt.min(r[i]);
r[i] -= repay;
debt -= repay;
let mut s = frac + r[i];
let mut cs = if frac.abs() >= r[i].abs() {
frac_comp + ((frac - s) + r[i])
} else {
frac_comp + ((r[i] - s) + frac)
};
let mut s_eff = s + cs;
if i < n_units - 1 && (1.0 - TAU..1.0).contains(&s_eff) {
debt += 1.0 - s_eff;
r[i] += 1.0 - s_eff;
s = 1.0;
cs = 0.0;
s_eff = 1.0;
adjusted_crossings += 1;
}
let crossing: i64;
if i == n_units - 1 {
crossing = s_eff.round_ties_even() as i64;
r[i] = crossing as f64 - f_prev;
frac = 0.0;
frac_comp = 0.0;
} else {
crossing = i64::from(s_eff >= 1.0);
frac = s - crossing as f64;
frac_comp = cs;
}
hits_cum_floor[i] = i_prev + k[i] as i64 + crossing;
remainder_after[i] = (frac + frac_comp).max(0.0);
let p_taken = if t_prev == i_prev {
if crossing == 0 {
r[i] / (1.0 - f_prev)
} else {
0.0
}
} else if crossing == 0 {
1.0
} else if f_prev > 0.0 {
remainder_after[i] / f_prev
} else {
0.0
};
taken[i] = hits_cum_floor[i] + i64::from(uniforms[i] < p_taken);
hits[i] = taken[i] - t_prev;
}
let effective_lambda: Vec<f64> = k.iter().zip(&r).map(|(ki, ri)| ki + ri).collect();
let lost = lambda
.iter()
.zip(&effective_lambda)
.filter(|&(&l, &e)| l > 0.0 && e <= 0.0)
.count();
if lost > 0 {
return Err(Error::LostProbability { units: lost });
}
let total: i64 = hits.iter().sum();
if total < 0 || total as u64 != n {
return Err(Error::Internal(format!(
"realised hits sum to {total}, expected {n}"
)));
}
for (i, &h) in hits.iter().enumerate() {
let lo = effective_lambda[i].floor() as i64;
let hi = effective_lambda[i].ceil() as i64;
if h < lo || h > hi {
return Err(Error::Internal(format!(
"unit {i} realised {h} hits against expected {}; minimum \
replacement was violated",
effective_lambda[i]
)));
}
}
let hits: Vec<u64> = hits.iter().map(|&h| h as u64).collect();
let max_adjustment = lambda
.iter()
.zip(&effective_lambda)
.map(|(&l, &e)| (e - l).abs())
.fold(0.0_f64, f64::max);
Ok(Selection {
hits,
effective_lambda,
sample_size: n,
eps_sum,
corrected_unit,
adjusted_crossings,
max_adjustment,
})
}