use prospicio_core::{Error, Result, StreamRng};
use prospicio_math::linalg::{cholesky, cholesky_solve};
pub fn stacking_weights(lpd: &[Vec<f64>]) -> Result<Vec<f64>> {
let n = check(lpd)?;
let k = lpd.len();
let mut e = vec![0.0; n * k];
for i in 0..n {
let m = lpd.iter().map(|l| l[i]).fold(f64::NEG_INFINITY, f64::max);
for j in 0..k {
e[i * k + j] = (lpd[j][i] - m).exp();
}
}
let mut w = vec![1.0 / k as f64; k];
for _ in 0..50 {
let g = gradient(&e, &w, n, k);
for (wj, gj) in w.iter_mut().zip(&g) {
*wj *= gj / n as f64;
}
}
let nf = n as f64;
for _round in 0..100 {
let mut support: Vec<usize> = (0..k).filter(|&j| w[j] > 0.0).collect();
newton(&e, &mut w, &mut support, n, k);
let g = gradient(&e, &w, n, k);
let on = support.iter().all(|&j| (g[j] - nf).abs() <= 1e-8 * nf);
let off = (0..k).all(|j| w[j] > 0.0 || g[j] <= nf * (1.0 + 1e-9));
if on && off {
return Ok(w);
}
if !off {
let j = (0..k)
.filter(|&j| w[j] == 0.0)
.max_by(|&a, &b| g[a].total_cmp(&g[b]))
.expect("a violated model is excluded");
w.iter_mut().for_each(|x| *x *= 0.99);
w[j] = 0.01;
}
}
Err(Error::Data("stacking weights did not converge".into()))
}
pub fn pseudo_bma_weights(lpd: &[Vec<f64>], bootstrap: Option<(usize, u64)>) -> Result<Vec<f64>> {
let n = check(lpd)?;
let k = lpd.len();
let Some((draws, seed)) = bootstrap else {
let elpd: Vec<f64> = lpd.iter().map(|l| l.iter().sum()).collect();
return Ok(softmax(&elpd));
};
if draws == 0 {
return Err(Error::InvalidParameter {
name: "draws",
value: 0.0,
reason: "must be positive",
});
}
let mut out = vec![0.0; k];
let mut g = vec![0.0; n];
for b in 0..draws {
let mut rng = StreamRng::new(seed, b as u64);
for x in g.iter_mut() {
*x = -rng.next_open01().ln();
}
let total: f64 = g.iter().sum();
let z: Vec<f64> = lpd
.iter()
.map(|l| n as f64 * l.iter().zip(&g).map(|(a, gi)| a * gi).sum::<f64>() / total)
.collect();
for (o, wk) in out.iter_mut().zip(softmax(&z)) {
*o += wk / draws as f64;
}
}
Ok(out)
}
fn softmax(z: &[f64]) -> Vec<f64> {
let m = z.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let e: Vec<f64> = z.iter().map(|v| (v - m).exp()).collect();
let s: f64 = e.iter().sum();
e.into_iter().map(|v| v / s).collect()
}
fn check(lpd: &[Vec<f64>]) -> Result<usize> {
if lpd.len() < 2 {
return Err(Error::Data("weights need at least two models".into()));
}
let n = lpd[0].len();
if n == 0 || lpd.iter().any(|l| l.len() != n) {
return Err(Error::Data(
"every model needs the same, non-zero number of pointwise log densities".into(),
));
}
if lpd.iter().flatten().any(|v| !v.is_finite()) {
return Err(Error::Data("pointwise log densities must be finite".into()));
}
Ok(n)
}
fn gradient(e: &[f64], w: &[f64], n: usize, k: usize) -> Vec<f64> {
let mut g = vec![0.0; k];
for row in e.chunks_exact(k).take(n) {
let s: f64 = row.iter().zip(w).map(|(a, b)| a * b).sum();
for (gj, a) in g.iter_mut().zip(row) {
*gj += a / s;
}
}
g
}
fn objective(e: &[f64], w: &[f64], k: usize) -> f64 {
e.chunks_exact(k)
.map(|row| row.iter().zip(w).map(|(a, b)| a * b).sum::<f64>().ln())
.sum()
}
fn newton(e: &[f64], w: &mut [f64], support: &mut Vec<usize>, n: usize, k: usize) {
for _ in 0..500 {
let m = support.len();
if m < 2 {
if let [only] = support[..] {
w.iter_mut().for_each(|x| *x = 0.0);
w[only] = 1.0;
}
return;
}
let mut g = vec![0.0; m];
let mut h = vec![0.0; m * m];
for row in e.chunks_exact(k).take(n) {
let s: f64 = row.iter().zip(w.iter()).map(|(a, b)| a * b).sum();
for (a, &ja) in support.iter().enumerate() {
let xa = row[ja] / s;
g[a] += xa;
for (b, &jb) in support.iter().enumerate() {
h[a * m + b] += xa * row[jb] / s;
}
}
}
let r = m - 1;
let grad: Vec<f64> = (0..r).map(|a| g[a] - g[r]).collect();
if grad.iter().all(|v| v.abs() <= 1e-12 * n as f64) {
return;
}
let mut neg_h = vec![0.0; r * r];
for a in 0..r {
for b in 0..r {
neg_h[a * r + b] = h[a * m + b] - h[a * m + r] - h[r * m + b] + h[r * m + r];
}
}
let scale = (0..r)
.map(|a| neg_h[a * r + a])
.fold(0.0, f64::max)
.max(1e-300);
let mut damping = 0.0;
let step = loop {
let mut a = neg_h.clone();
for d in 0..r {
a[d * r + d] += damping;
}
if let Some(l) = cholesky(&a, r) {
break cholesky_solve(&l, &grad);
}
damping = if damping == 0.0 {
1e-12 * scale
} else {
damping * 10.0
};
};
let mut full = vec![0.0; m];
full[..r].copy_from_slice(&step);
full[r] = -step.iter().sum::<f64>();
let mut t_max = f64::INFINITY;
let mut blocking = None;
for (a, &ja) in support.iter().enumerate() {
if full[a] < 0.0 {
let t = -w[ja] / full[a];
if t < t_max {
t_max = t;
blocking = Some(a);
}
}
}
let base = objective(e, w, k);
let mut t = t_max.min(1.0);
let mut moved = false;
for _ in 0..60 {
let mut trial = w.to_vec();
for (a, &ja) in support.iter().enumerate() {
trial[ja] = (w[ja] + t * full[a]).max(0.0);
}
if objective(e, &trial, k) >= base {
w.copy_from_slice(&trial);
moved = true;
break;
}
t *= 0.5;
}
if !moved {
return;
}
if t == t_max {
if let Some(a) = blocking {
w[support[a]] = 0.0;
support.remove(a);
}
}
let s: f64 = w.iter().sum();
w.iter_mut().for_each(|x| *x /= s);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_dominated_model_gets_no_stacking_weight() {
let a = vec![-1.0, -1.2, -0.8, -1.1];
let worse: Vec<f64> = a.iter().map(|v| v - 0.5).collect();
let w = stacking_weights(&[a, worse]).unwrap();
assert_eq!(w, vec![1.0, 0.0]);
}
#[test]
fn pseudo_bma_is_a_softmax_of_elpd_and_the_bootstrap_shrinks_it() {
let a = vec![-1.0, -2.0, -1.5, -1.0, -0.5, -2.5];
let b = vec![-1.2, -1.6, -1.4, -1.3, -0.9, -1.8];
let w = pseudo_bma_weights(&[a.clone(), b.clone()], None).unwrap();
let (ea, eb): (f64, f64) = (a.iter().sum(), b.iter().sum());
assert!((w[0] - 1.0 / (1.0 + (eb - ea).exp())).abs() < 1e-15);
let plus = pseudo_bma_weights(&[a.clone(), b.clone()], Some((4000, 1))).unwrap();
assert!((plus[0] + plus[1] - 1.0).abs() < 1e-12);
assert!(plus[1] > 0.5 && plus[1] < w[1]);
assert_eq!(plus, pseudo_bma_weights(&[a, b], Some((4000, 1))).unwrap());
}
#[test]
fn rejects_bad_input() {
assert!(stacking_weights(&[vec![-1.0]]).is_err());
assert!(stacking_weights(&[vec![-1.0], vec![-1.0, -2.0]]).is_err());
assert!(stacking_weights(&[vec![f64::NAN], vec![-1.0]]).is_err());
assert!(pseudo_bma_weights(&[vec![-1.0], vec![-1.0]], Some((0, 1))).is_err());
}
}