1use prospicio_core::{Error, Result, StreamRng};
14use prospicio_math::linalg::{cholesky, cholesky_solve};
15
16pub fn stacking_weights(lpd: &[Vec<f64>]) -> Result<Vec<f64>> {
37 let n = check(lpd)?;
38 let k = lpd.len();
39 let mut e = vec![0.0; n * k];
41 for i in 0..n {
42 let m = lpd.iter().map(|l| l[i]).fold(f64::NEG_INFINITY, f64::max);
43 for j in 0..k {
44 e[i * k + j] = (lpd[j][i] - m).exp();
45 }
46 }
47 let mut w = vec![1.0 / k as f64; k];
48 for _ in 0..50 {
51 let g = gradient(&e, &w, n, k);
52 for (wj, gj) in w.iter_mut().zip(&g) {
53 *wj *= gj / n as f64;
54 }
55 }
56 let nf = n as f64;
57 for _round in 0..100 {
58 let mut support: Vec<usize> = (0..k).filter(|&j| w[j] > 0.0).collect();
59 newton(&e, &mut w, &mut support, n, k);
60 let g = gradient(&e, &w, n, k);
62 let on = support.iter().all(|&j| (g[j] - nf).abs() <= 1e-8 * nf);
63 let off = (0..k).all(|j| w[j] > 0.0 || g[j] <= nf * (1.0 + 1e-9));
64 if on && off {
65 return Ok(w);
66 }
67 if !off {
69 let j = (0..k)
70 .filter(|&j| w[j] == 0.0)
71 .max_by(|&a, &b| g[a].total_cmp(&g[b]))
72 .expect("a violated model is excluded");
73 w.iter_mut().for_each(|x| *x *= 0.99);
74 w[j] = 0.01;
75 }
76 }
77 Err(Error::Data("stacking weights did not converge".into()))
78}
79
80pub fn pseudo_bma_weights(lpd: &[Vec<f64>], bootstrap: Option<(usize, u64)>) -> Result<Vec<f64>> {
89 let n = check(lpd)?;
90 let k = lpd.len();
91 let Some((draws, seed)) = bootstrap else {
92 let elpd: Vec<f64> = lpd.iter().map(|l| l.iter().sum()).collect();
93 return Ok(softmax(&elpd));
94 };
95 if draws == 0 {
96 return Err(Error::InvalidParameter {
97 name: "draws",
98 value: 0.0,
99 reason: "must be positive",
100 });
101 }
102 let mut out = vec![0.0; k];
103 let mut g = vec![0.0; n];
104 for b in 0..draws {
105 let mut rng = StreamRng::new(seed, b as u64);
106 for x in g.iter_mut() {
108 *x = -rng.next_open01().ln();
109 }
110 let total: f64 = g.iter().sum();
111 let z: Vec<f64> = lpd
112 .iter()
113 .map(|l| n as f64 * l.iter().zip(&g).map(|(a, gi)| a * gi).sum::<f64>() / total)
114 .collect();
115 for (o, wk) in out.iter_mut().zip(softmax(&z)) {
116 *o += wk / draws as f64;
117 }
118 }
119 Ok(out)
120}
121
122fn softmax(z: &[f64]) -> Vec<f64> {
123 let m = z.iter().copied().fold(f64::NEG_INFINITY, f64::max);
124 let e: Vec<f64> = z.iter().map(|v| (v - m).exp()).collect();
125 let s: f64 = e.iter().sum();
126 e.into_iter().map(|v| v / s).collect()
127}
128
129fn check(lpd: &[Vec<f64>]) -> Result<usize> {
130 if lpd.len() < 2 {
131 return Err(Error::Data("weights need at least two models".into()));
132 }
133 let n = lpd[0].len();
134 if n == 0 || lpd.iter().any(|l| l.len() != n) {
135 return Err(Error::Data(
136 "every model needs the same, non-zero number of pointwise log densities".into(),
137 ));
138 }
139 if lpd.iter().flatten().any(|v| !v.is_finite()) {
140 return Err(Error::Data("pointwise log densities must be finite".into()));
141 }
142 Ok(n)
143}
144
145fn gradient(e: &[f64], w: &[f64], n: usize, k: usize) -> Vec<f64> {
147 let mut g = vec![0.0; k];
148 for row in e.chunks_exact(k).take(n) {
149 let s: f64 = row.iter().zip(w).map(|(a, b)| a * b).sum();
150 for (gj, a) in g.iter_mut().zip(row) {
151 *gj += a / s;
152 }
153 }
154 g
155}
156
157fn objective(e: &[f64], w: &[f64], k: usize) -> f64 {
158 e.chunks_exact(k)
159 .map(|row| row.iter().zip(w).map(|(a, b)| a * b).sum::<f64>().ln())
160 .sum()
161}
162
163fn newton(e: &[f64], w: &mut [f64], support: &mut Vec<usize>, n: usize, k: usize) {
167 for _ in 0..500 {
168 let m = support.len();
169 if m < 2 {
170 if let [only] = support[..] {
171 w.iter_mut().for_each(|x| *x = 0.0);
172 w[only] = 1.0;
173 }
174 return;
175 }
176 let mut g = vec![0.0; m];
177 let mut h = vec![0.0; m * m];
178 for row in e.chunks_exact(k).take(n) {
179 let s: f64 = row.iter().zip(w.iter()).map(|(a, b)| a * b).sum();
180 for (a, &ja) in support.iter().enumerate() {
181 let xa = row[ja] / s;
182 g[a] += xa;
183 for (b, &jb) in support.iter().enumerate() {
184 h[a * m + b] += xa * row[jb] / s;
185 }
186 }
187 }
188 let r = m - 1;
190 let grad: Vec<f64> = (0..r).map(|a| g[a] - g[r]).collect();
191 if grad.iter().all(|v| v.abs() <= 1e-12 * n as f64) {
192 return;
193 }
194 let mut neg_h = vec![0.0; r * r];
195 for a in 0..r {
196 for b in 0..r {
197 neg_h[a * r + b] = h[a * m + b] - h[a * m + r] - h[r * m + b] + h[r * m + r];
198 }
199 }
200 let scale = (0..r)
202 .map(|a| neg_h[a * r + a])
203 .fold(0.0, f64::max)
204 .max(1e-300);
205 let mut damping = 0.0;
206 let step = loop {
207 let mut a = neg_h.clone();
208 for d in 0..r {
209 a[d * r + d] += damping;
210 }
211 if let Some(l) = cholesky(&a, r) {
212 break cholesky_solve(&l, &grad);
213 }
214 damping = if damping == 0.0 {
215 1e-12 * scale
216 } else {
217 damping * 10.0
218 };
219 };
220 let mut full = vec![0.0; m];
221 full[..r].copy_from_slice(&step);
222 full[r] = -step.iter().sum::<f64>();
223 let mut t_max = f64::INFINITY;
225 let mut blocking = None;
226 for (a, &ja) in support.iter().enumerate() {
227 if full[a] < 0.0 {
228 let t = -w[ja] / full[a];
229 if t < t_max {
230 t_max = t;
231 blocking = Some(a);
232 }
233 }
234 }
235 let base = objective(e, w, k);
236 let mut t = t_max.min(1.0);
237 let mut moved = false;
238 for _ in 0..60 {
239 let mut trial = w.to_vec();
240 for (a, &ja) in support.iter().enumerate() {
241 trial[ja] = (w[ja] + t * full[a]).max(0.0);
242 }
243 if objective(e, &trial, k) >= base {
244 w.copy_from_slice(&trial);
245 moved = true;
246 break;
247 }
248 t *= 0.5;
249 }
250 if !moved {
251 return;
252 }
253 if t == t_max {
254 if let Some(a) = blocking {
255 w[support[a]] = 0.0;
256 support.remove(a);
257 }
258 }
259 let s: f64 = w.iter().sum();
260 w.iter_mut().for_each(|x| *x /= s);
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn a_dominated_model_gets_no_stacking_weight() {
270 let a = vec![-1.0, -1.2, -0.8, -1.1];
271 let worse: Vec<f64> = a.iter().map(|v| v - 0.5).collect();
272 let w = stacking_weights(&[a, worse]).unwrap();
273 assert_eq!(w, vec![1.0, 0.0]);
274 }
275
276 #[test]
277 fn pseudo_bma_is_a_softmax_of_elpd_and_the_bootstrap_shrinks_it() {
278 let a = vec![-1.0, -2.0, -1.5, -1.0, -0.5, -2.5];
279 let b = vec![-1.2, -1.6, -1.4, -1.3, -0.9, -1.8];
280 let w = pseudo_bma_weights(&[a.clone(), b.clone()], None).unwrap();
281 let (ea, eb): (f64, f64) = (a.iter().sum(), b.iter().sum());
282 assert!((w[0] - 1.0 / (1.0 + (eb - ea).exp())).abs() < 1e-15);
283 let plus = pseudo_bma_weights(&[a.clone(), b.clone()], Some((4000, 1))).unwrap();
284 assert!((plus[0] + plus[1] - 1.0).abs() < 1e-12);
285 assert!(plus[1] > 0.5 && plus[1] < w[1]);
288 assert_eq!(plus, pseudo_bma_weights(&[a, b], Some((4000, 1))).unwrap());
289 }
290
291 #[test]
292 fn rejects_bad_input() {
293 assert!(stacking_weights(&[vec![-1.0]]).is_err());
294 assert!(stacking_weights(&[vec![-1.0], vec![-1.0, -2.0]]).is_err());
295 assert!(stacking_weights(&[vec![f64::NAN], vec![-1.0]]).is_err());
296 assert!(pseudo_bma_weights(&[vec![-1.0], vec![-1.0]], Some((0, 1))).is_err());
297 }
298}