1use super::{Acc, Format, Sink, summary_quantile};
6use crate::continuous::Mixture;
7use crate::value::Value;
8use crate::weight::Weight;
9use probl_sema::ir::{ReportKind, ReportSite};
10
11#[derive(Clone, Debug)]
13pub struct ReportResult {
14 pub reach: Option<Reach>,
18 pub groups: Vec<GroupResult>,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub struct Reach {
24 pub share: f64,
25 pub se: Option<f64>,
27}
28
29#[derive(Clone, Debug)]
31pub struct GroupResult {
32 pub key: Value,
33 pub fact: Option<Quantity>,
35 pub values: Option<Vec<(Value, Quantity)>>,
39 pub distribution: Vec<(Value, f64)>,
42 pub numeric: Option<Numeric>,
44 pub unresolved_share: f64,
47 pub support: Option<Support>,
49}
50
51#[derive(Clone, Copy, Debug, PartialEq)]
53pub struct Support {
54 pub contributing_runs: u64,
56 pub effective: f64,
58}
59
60#[derive(Clone, Copy, Debug, PartialEq)]
62pub struct Quantity {
63 pub point: Option<f64>,
65 pub complete: bool,
67 pub bounds: Option<(f64, f64)>,
70 pub sampling: Option<Uncertainty>,
72}
73
74#[derive(Clone, Copy, Debug, PartialEq)]
75pub struct Uncertainty {
76 pub status: Status,
77 pub se: Option<f64>,
79 pub wilson: Option<(f64, f64)>,
81 pub support: Support,
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
86pub enum Status {
87 Estimated,
89 NotEstimable,
92 NotComputed,
94 IntegratedZero,
96}
97
98#[derive(Clone, Debug)]
100pub struct Numeric {
101 pub mean: Quantity,
102 pub sd: Quantity,
103 pub percent: bool,
105 shape: Shape,
106 complete: bool,
108 support: Option<Support>,
109}
110
111#[derive(Clone, Debug)]
112enum Shape {
113 Mixture(Mixture),
115 Points(Vec<(Value, f64)>),
117}
118
119impl Numeric {
120 pub fn mixture(&self) -> Option<&Mixture> {
122 match &self.shape {
123 Shape::Mixture(m) => Some(m),
124 Shape::Points(_) => None,
125 }
126 }
127
128 pub fn quantile(&self, q: f64) -> Option<Quantity> {
132 let point = match &self.shape {
133 Shape::Mixture(m) => Some(if q == 0.5 { m.median() } else { m.quantile(q) }),
134 Shape::Points(points) => summary_quantile(points, q).and_then(|v| v.as_f64()),
135 }?;
136 Some(Quantity {
137 point: Some(point),
138 complete: self.complete,
139 bounds: None,
140 sampling: self.support.map(not_computed),
141 })
142 }
143
144 pub fn quantile_value(&self, q: f64) -> Option<Value> {
147 match &self.shape {
148 Shape::Points(points) => summary_quantile(points, q),
149 Shape::Mixture(_) => None,
150 }
151 }
152
153 pub(super) fn points(&self) -> Option<Vec<(f64, f64)>> {
155 match &self.shape {
156 Shape::Points(points) => Some(
157 points
158 .iter()
159 .map(|(v, p)| (v.as_f64().unwrap_or(f64::NAN), *p))
160 .collect(),
161 ),
162 Shape::Mixture(_) => None,
163 }
164 }
165}
166
167fn not_computed(support: Support) -> Uncertainty {
168 Uncertainty {
169 status: Status::NotComputed,
170 se: None,
171 wilson: None,
172 support,
173 }
174}
175
176pub fn results(sites: &[ReportSite], sinks: &[Sink], format: Format, unresolved: Weight) -> Vec<ReportResult> {
180 sites
181 .iter()
182 .zip(sinks)
183 .map(|(site, sink)| {
184 let kind = if site.key_label.is_none() {
187 ReportKind::Once
188 } else {
189 site.kind
190 };
191 ReportResult {
192 reach: reach(site.kind, sink, format),
193 groups: sink
194 .groups
195 .iter()
196 .map(|(key, acc)| group(key, acc, format, kind, unresolved))
197 .collect(),
198 }
199 })
200 .collect()
201}
202
203fn reach(kind: ReportKind, sink: &Sink, format: Format) -> Option<Reach> {
204 if kind == ReportKind::PerVisit || sink.groups.is_empty() || format.program_total.is_zero() {
205 return None;
206 }
207 if let Some(all_squares) = format.run_squares {
208 let total = format.program_total;
211 let p = sink.reached.ratio(total);
212 let squares = sink.reached_squares;
213 let spread = squares.scale((1.0 - p) * (1.0 - p)) + all_squares.saturating_sub(squares).scale(p * p);
214 let se = spread.ratio(total * total).sqrt();
215 return Some(Reach { share: p, se: Some(se) });
216 }
217 Some(Reach {
218 share: sink.reach().ratio(format.program_total),
219 se: None,
220 })
221}
222
223pub(super) fn group(key: &Value, acc: &Acc, format: Format, kind: ReportKind, unresolved: Weight) -> GroupResult {
224 let sampled = acc.sampled();
225 let support = sampled.then(|| Support {
226 contributing_runs: acc.contributing_runs(),
227 effective: acc.effective(),
228 });
229 let complete = (unresolved + acc.missing).is_zero();
230 let distribution = acc.distribution();
231 let fact = acc
232 .is_event()
233 .then(|| fact(acc, format, kind, complete, unresolved, support));
234 let continuous = distribution
235 .iter()
236 .any(|(v, _)| matches!(v, Value::Analytic(_) | Value::Continuous(_)));
237 let values = (!continuous).then(|| values(acc, &distribution, complete, unresolved, support));
238 let numeric = if acc.is_event() {
239 None
240 } else {
241 numeric(acc, &distribution, complete, support)
242 };
243 GroupResult {
244 key: key.clone(),
245 fact,
246 values,
247 numeric,
248 unresolved_share: acc.unresolved_share(format.unresolved),
249 distribution,
250 support,
251 }
252}
253
254fn fact(
256 acc: &Acc,
257 format: Format,
258 kind: ReportKind,
259 complete: bool,
260 unresolved: Weight,
261 support: Option<Support>,
262) -> Quantity {
263 let p = acc.chance();
264 let sampling = support.map(|support| {
265 let se = acc.chance_se();
266 let wilson = acc
270 .chance_interval95(kind)
271 .filter(|_| !format.weighted)
272 .filter(|_| p == 0.0 || p == 1.0 || support.contributing_runs < 30);
273 let integrated = acc.moments.as_ref().is_some_and(|m| m.integrated);
274 let (status, se) = if se != 0.0 {
275 (Status::Estimated, Some(se))
276 } else if integrated {
277 (Status::IntegratedZero, Some(0.0))
278 } else {
279 (Status::NotEstimable, None)
280 };
281 Uncertainty {
282 status,
283 se,
284 wilson,
285 support,
286 }
287 });
288 Quantity {
289 point: Some(p),
290 complete,
291 bounds: (!complete && support.is_none()).then(|| acc.chance_bounds(unresolved)),
292 sampling,
293 }
294}
295
296fn values(
299 acc: &Acc,
300 distribution: &[(Value, f64)],
301 complete: bool,
302 unresolved: Weight,
303 support: Option<Support>,
304) -> Vec<(Value, Quantity)> {
305 let u = unresolved + acc.missing;
306 let resolved = Weight::sum(acc.values.values().copied()) + acc.facts;
307 distribution
308 .iter()
309 .map(|(value, p)| {
310 let bounds = (!complete && support.is_none()).then(|| {
311 let w = match value {
313 Value::Bool(true) => acc.yes,
314 Value::Bool(false) => acc.facts.saturating_sub(acc.yes),
315 _ => acc.values.get(value).copied().unwrap_or_default(),
316 };
317 let total = resolved + u;
318 (w.ratio(total), (w + u).ratio(total).min(1.0))
319 });
320 let sampling = support.map(|support| {
321 let se = acc.value_se(value, *p);
322 let (status, se) = if se == 0.0 {
323 (Status::NotEstimable, None)
324 } else {
325 (Status::Estimated, Some(se))
326 };
327 Uncertainty {
328 status,
329 se,
330 wilson: None,
331 support,
332 }
333 });
334 let quantity = Quantity {
335 point: Some(*p),
336 complete,
337 bounds,
338 sampling,
339 };
340 (value.clone(), quantity)
341 })
342 .collect()
343}
344
345fn numeric(acc: &Acc, distribution: &[(Value, f64)], complete: bool, support: Option<Support>) -> Option<Numeric> {
348 let summary = |mean: f64, sd: f64, mean_sampling: Option<Uncertainty>, percent: bool, shape: Shape| {
349 let quantity = |x: f64, sampling| Quantity {
350 point: Some(x),
351 complete,
352 bounds: None,
353 sampling,
354 };
355 Some(Numeric {
356 mean: quantity(mean, mean_sampling),
357 sd: quantity(sd, support.map(not_computed)),
358 percent,
359 shape,
360 complete,
361 support,
362 })
363 };
364 if let Some(m) = super::analytic_mixture(distribution) {
365 let (mean, sd) = (m.mean(), m.sd());
366 return summary(mean, sd, support.map(not_computed), false, Shape::Mixture(m));
367 }
368 let nums: Vec<(f64, f64)> = distribution
369 .iter()
370 .map(|(v, p)| match v {
371 Value::Int(n) => n
372 .to_f64()
373 .filter(|x| n.cmp_f64(*x).is_some_and(|c| c.is_eq()))
374 .map(|x| (x, *p)),
375 Value::Float(x) | Value::Prob(x) => Some((*x, *p)),
376 _ => None,
377 })
378 .collect::<Option<_>>()?;
379 let percent = distribution.iter().all(|(v, _)| matches!(v, Value::Prob(_)));
380 let total: f64 = nums.iter().map(|(_, p)| p).sum();
381 let mean = nums.iter().map(|(x, p)| x * p).sum::<f64>() / total;
382 let sd = (nums.iter().map(|(x, p)| (x - mean).powi(2) * p).sum::<f64>() / total).sqrt();
383 if !mean.is_finite() || !sd.is_finite() {
384 return None;
385 }
386 let mean_sampling = support.map(|support| {
387 let se = acc.mean_se().1;
388 let (status, se) = if se > 0.0 {
389 (Status::Estimated, Some(se))
390 } else {
391 (Status::NotEstimable, None)
392 };
393 Uncertainty {
394 status,
395 se,
396 wilson: None,
397 support,
398 }
399 });
400 summary(mean, sd, mean_sampling, percent, Shape::Points(distribution.to_vec()))
401}