use crate::error::Error;
const BOUNDARY_TOL: f64 = 1e-9;
struct Strata {
kind: Vec<UnitKind>,
b: Vec<f64>,
c_prefix: Vec<f64>,
zeros_up_to: Vec<usize>,
}
#[derive(Clone, Copy, PartialEq)]
enum UnitKind {
Interior(usize),
Cross(usize),
}
impl Strata {
fn build(pi: &[f64], n: u64) -> Strata {
let n_units = pi.len();
let mut kind = vec![UnitKind::Interior(1); n_units];
let mut b = vec![0.0_f64; n as usize + 1];
let mut c = vec![0.0_f64; n as usize];
let mut v = 0.0_f64;
let mut comp = 0.0_f64;
let mut v_eff = 0.0_f64; let mut boundary = 0_usize; for (k, &p) in pi.iter().enumerate() {
let v_prev = v_eff;
let t = v + p;
if v.abs() >= p.abs() {
comp += (v - t) + p;
} else {
comp += (p - t) + v;
}
v = t;
let mut v_now = v + comp;
if (v_now - v_now.round()).abs() < BOUNDARY_TOL {
v_now = v_now.round();
}
v_eff = v_now;
let next_int = (boundary + 1) as f64;
if v_now >= next_int && boundary + 1 < n as usize {
boundary += 1;
let a = next_int - v_prev;
let bm = v_now - next_int;
if bm <= BOUNDARY_TOL {
kind[k] = UnitKind::Interior(boundary);
b[boundary] = 0.0;
c[boundary] = 0.0;
} else {
kind[k] = UnitKind::Cross(boundary);
b[boundary] = bm;
c[boundary] = if a <= BOUNDARY_TOL {
0.0
} else {
a * bm / ((1.0 - a) * (1.0 - bm))
};
}
} else {
kind[k] = UnitKind::Interior(boundary + 1);
}
}
let m = c.len(); let mut c_prefix = vec![1.0_f64; m + 1];
let mut zeros_up_to = vec![0_usize; m + 1];
for t in 1..=m.saturating_sub(1) {
let ct = c[t];
zeros_up_to[t] = zeros_up_to[t - 1] + usize::from(ct == 0.0);
c_prefix[t] = if ct == 0.0 {
c_prefix[t - 1]
} else {
c_prefix[t - 1] * ct
};
}
if m > 0 {
zeros_up_to[m] = zeros_up_to[m.saturating_sub(1)];
c_prefix[m] = c_prefix[m.saturating_sub(1)];
}
Strata {
kind,
b,
c_prefix,
zeros_up_to,
}
}
fn coupling(&self, i: usize, j: usize) -> f64 {
if i >= j {
return 1.0;
}
if self.zeros_up_to[j - 1] > self.zeros_up_to[i - 1] {
return 0.0;
}
self.c_prefix[j - 1] / self.c_prefix[i - 1]
}
fn joint(&self, k: usize, l: usize, pk: f64, pl: f64) -> f64 {
if pk == 0.0 || pl == 0.0 {
return 0.0;
}
match (self.kind[k], self.kind[l]) {
(UnitKind::Interior(i), UnitKind::Interior(j)) => {
if i == j {
0.0
} else {
pk * pl * (1.0 - self.coupling(i, j))
}
}
(UnitKind::Cross(m), UnitKind::Interior(j)) => {
let i = m + 1;
let bm = self.b[m];
let correction = if pk >= 1.0 || bm >= 1.0 - BOUNDARY_TOL {
0.0
} else {
bm * (1.0 - pk) / (pk * (1.0 - bm)) * self.coupling(i, j)
};
pk * pl * (1.0 - correction)
}
(UnitKind::Interior(i), UnitKind::Cross(m)) => {
let j = m + 1;
let bm = self.b[m];
let correction = if pl >= 1.0 || bm <= BOUNDARY_TOL {
0.0
} else {
(1.0 - pl) * (1.0 - bm) / (pl * bm) * self.coupling(i, j)
};
pk * pl * (1.0 - correction)
}
(UnitKind::Cross(mk), UnitKind::Cross(ml)) => {
let i = mk + 1;
let j = ml + 1;
let bi = self.b[mk];
let bj = self.b[ml];
let correction =
if pk >= 1.0 || pl >= 1.0 || bi >= 1.0 - BOUNDARY_TOL || bj <= BOUNDARY_TOL {
0.0
} else {
bi * (1.0 - bj) * (1.0 - pk) * (1.0 - pl) / (pk * pl * bj * (1.0 - bi))
* self.coupling(i, j)
};
pk * pl * (1.0 - correction)
}
}
}
}
fn validate(pi: &[f64], order: &[usize]) -> Result<u64, Error> {
if order.len() != pi.len() {
return Err(Error::Input(format!(
"{} probabilities but an order of {} positions",
pi.len(),
order.len()
)));
}
for (i, &p) in pi.iter().enumerate() {
if !p.is_finite() || !(0.0..=1.0).contains(&p) {
return Err(Error::Input(format!(
"joint inclusion probabilities are defined for a without-replacement \
design: every probability in [0, 1]; unit {i} has {p}"
)));
}
}
let mut sum = 0.0_f64;
let mut comp = 0.0_f64;
for &p in pi {
let t = sum + p;
if sum.abs() >= p.abs() {
comp += (sum - t) + p;
} else {
comp += (p - t) + sum;
}
sum = t;
}
let n = (sum + comp).round_ties_even();
if ((sum - n) + comp).abs() >= 1e-6 {
return Err(Error::Drift {
eps: (sum - n) + comp,
});
}
if n < 1.0 {
return Err(Error::Input(
"the probabilities sum to zero; there is no design".into(),
));
}
Ok(n as u64)
}
pub fn joint_inclusion(
pi: &[f64],
order: &[usize],
units: &[usize],
randomized_head: bool,
) -> Result<Vec<f64>, Error> {
let n = validate(pi, order)?;
let n_units = pi.len();
let mut seen = vec![false; n_units];
for &u in order {
if u >= n_units || seen[u] {
return Err(Error::Input(
"the order must be a permutation of the frame".into(),
));
}
seen[u] = true;
}
for &u in units {
if u >= n_units {
return Err(Error::Input(format!(
"unit {u} is beyond the {n_units} units of the frame"
)));
}
}
let m = units.len();
let mut matrix = vec![0.0_f64; m * m];
for (r, &u) in units.iter().enumerate() {
matrix[r * m + r] = pi[u];
}
if m < 2 {
return Ok(matrix);
}
let mut position = vec![0_usize; n_units];
for (p, &u) in order.iter().enumerate() {
position[u] = p;
}
let listed: Vec<f64> = order.iter().map(|&u| pi[u]).collect();
let accumulate = |rotation_start: usize, weight: f64, matrix: &mut [f64]| {
let rotated: Vec<f64> = (0..n_units)
.map(|p| listed[(p + rotation_start) % n_units])
.collect();
let strata = Strata::build(&rotated, n);
for r in 0..m {
for s in (r + 1)..m {
let pr = (position[units[r]] + n_units - rotation_start) % n_units;
let ps = (position[units[s]] + n_units - rotation_start) % n_units;
let (k, l) = if pr < ps { (pr, ps) } else { (ps, pr) };
let value = strata.joint(k, l, rotated[k], rotated[l]);
matrix[r * m + s] += weight * value;
matrix[s * m + r] += weight * value;
}
}
};
if randomized_head {
let n_f = n as f64;
for (start, &p_start) in listed.iter().enumerate() {
let weight = p_start / n_f;
if weight > 0.0 {
accumulate(start, weight, &mut matrix);
}
}
} else {
accumulate(0, 1.0, &mut matrix);
}
Ok(matrix)
}