1use prospicio_core::{Error, Result};
7use prospicio_prob::{ComponentKey, KeyValue, PredictiveDistribution, Provenance};
8
9use crate::family::Family;
10
11pub fn from_means(
33 family: Family,
34 means: &[Vec<f64>],
35 dispersion: &[f64],
36 weights: &[f64],
37 n_sims: usize,
38 seed: u64,
39 provenance: Provenance,
40) -> Result<PredictiveDistribution> {
41 family.validate()?;
42 let Some(n) = means.first().map(Vec::len) else {
43 return Err(Error::Data("needs at least one mean vector".into()));
44 };
45 if n == 0 || means.iter().any(|m| m.len() != n) {
46 return Err(Error::Data(
47 "every mean vector needs the same, non-zero number of rows".into(),
48 ));
49 }
50 if let Some(&bad) = means.iter().flatten().find(|&&m| !family.valid_mu(m)) {
51 return Err(Error::InvalidParameter {
52 name: "means",
53 value: bad,
54 reason: "outside the family's range",
55 });
56 }
57 if dispersion.len() != 1 && dispersion.len() != n {
58 return Err(Error::Data(format!(
59 "dispersion needs 1 or {n} values, got {}",
60 dispersion.len()
61 )));
62 }
63 if let Some(&bad) = dispersion.iter().find(|&&d| !(d.is_finite() && d > 0.0)) {
64 return Err(Error::InvalidParameter {
65 name: "dispersion",
66 value: bad,
67 reason: "must be positive and finite",
68 });
69 }
70 let phi = |i: usize| dispersion[if dispersion.len() == 1 { 0 } else { i }];
71 let ones = vec![1.0; n];
72 let weights = if weights.is_empty() {
73 &ones[..]
74 } else {
75 weights
76 };
77 if weights.len() != n || weights.iter().any(|w| !(w.is_finite() && *w > 0.0)) {
78 return Err(Error::Data(format!(
79 "weights need {n} positive finite values"
80 )));
81 }
82 let k = means.len();
83 let components: Vec<ComponentKey> = (0..n).map(|i| vec![KeyValue::from(i as i64)]).collect();
84 let provenance = provenance
85 .param("family", family.name())
86 .param(
87 "dispersion",
88 if dispersion.len() == 1 {
89 dispersion[0].to_string()
90 } else {
91 "per row".to_string()
92 },
93 )
94 .param("mean_vectors", k);
95 PredictiveDistribution::simulate(
99 vec!["row".into()],
100 components,
101 n_sims,
102 seed,
103 provenance,
104 |rng, row| {
105 let mu = &means[((rng.next_open01() * k as f64) as usize).min(k - 1)];
106 for (i, (out, (&m, &w))) in row.iter_mut().zip(mu.iter().zip(weights)).enumerate() {
107 *out = family
108 .draw(m, phi(i), w, rng.next_open01())
109 .unwrap_or(f64::NAN);
110 }
111 },
112 )
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use prospicio_prob::Distribution;
119
120 #[test]
121 fn draws_centre_on_the_means_with_the_family_variance() {
122 let pd = from_means(
123 Family::Gamma,
124 &[vec![100.0, 300.0]],
125 &[0.5],
126 &[1.0, 2.0],
127 40_000,
128 3,
129 Provenance::new("test"),
130 )
131 .unwrap();
132 for (j, (mu, w)) in [(100.0, 1.0), (300.0, 2.0)].into_iter().enumerate() {
134 let m = pd.marginal(&vec![KeyValue::from(j as i64)]).unwrap();
135 assert!((m.mean() / mu - 1.0).abs() < 0.02, "{j}");
136 assert!(
137 (m.variance() / (0.5 * mu * mu / w) - 1.0).abs() < 0.05,
138 "{j}"
139 );
140 }
141 }
142
143 #[test]
144 fn several_mean_vectors_add_their_spread() {
145 let means = [vec![2.0], vec![6.0]];
148 let pd = from_means(
149 Family::Gaussian,
150 &means,
151 &[1.0],
152 &[],
153 40_000,
154 5,
155 Provenance::new("test"),
156 )
157 .unwrap();
158 let total = pd.total();
159 assert!((total.mean() - 4.0).abs() < 0.05);
160 assert!((total.variance() - (1.0 + 4.0)).abs() < 0.15);
161 assert_eq!(pd.provenance().seed, Some(5));
162 }
163
164 #[test]
165 fn per_row_dispersion() {
166 let pd = from_means(
168 Family::Gamma,
169 &[vec![100.0, 100.0]],
170 &[0.1, 0.4],
171 &[],
172 40_000,
173 9,
174 Provenance::new("test"),
175 )
176 .unwrap();
177 let v = |j: i64| pd.marginal(&vec![KeyValue::from(j)]).unwrap().variance();
178 assert!((v(0) / 1000.0 - 1.0).abs() < 0.05);
179 assert!((v(1) / 4000.0 - 1.0).abs() < 0.05);
180 let p = || Provenance::new("test");
181 assert!(
182 from_means(
183 Family::Gamma,
184 &[vec![1.0, 2.0]],
185 &[1.0, 1.0, 1.0],
186 &[],
187 10,
188 1,
189 p()
190 )
191 .is_err()
192 );
193 assert!(
194 from_means(
195 Family::Gamma,
196 &[vec![1.0, 2.0]],
197 &[1.0, -1.0],
198 &[],
199 10,
200 1,
201 p()
202 )
203 .is_err()
204 );
205 }
206
207 #[test]
208 fn rejects_bad_input() {
209 let p = || Provenance::new("test");
210 assert!(from_means(Family::Poisson, &[], &[1.0], &[], 10, 1, p()).is_err());
211 assert!(
212 from_means(
213 Family::Poisson,
214 &[vec![1.0], vec![]],
215 &[1.0],
216 &[],
217 10,
218 1,
219 p()
220 )
221 .is_err()
222 );
223 assert!(from_means(Family::Poisson, &[vec![-1.0]], &[1.0], &[], 10, 1, p()).is_err());
224 assert!(from_means(Family::Poisson, &[vec![1.0]], &[0.0], &[], 10, 1, p()).is_err());
225 assert!(
226 from_means(
227 Family::Poisson,
228 &[vec![1.0]],
229 &[1.0],
230 &[1.0, 2.0],
231 10,
232 1,
233 p()
234 )
235 .is_err()
236 );
237 }
238}