vj_sim/covergroup/compute.rs
1//! Coverage computation (IEEE 1800-2023 19.11): the weighted average, the
2//! zero-denominator rules and the merge of instances by bin name.
3
4use std::collections::BTreeMap;
5
6use super::coverpoint::Coverage;
7
8/// The result of one `get_coverage` / `get_inst_coverage` query.
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct Score {
11 /// The returned coverage number, 0.0 to 100.0.
12 pub percent: f64,
13 /// The `covered` output of the two-argument forms.
14 pub covered: u64,
15 /// The `total` output of the two-argument forms.
16 pub total: u64,
17 /// The item has a nonzero denominator: it takes part in its parent's
18 /// weighted average. Otherwise it is excluded from numerator and
19 /// denominator alike (19.11).
20 pub contributes: bool,
21}
22
23/// What a query returns when its denominator is zero (19.11, rules b and c):
24/// 0.0 for a nonzero weight, 100.0 for a zero weight; both outputs 0 (rule d).
25pub fn empty_score(weight: i64) -> Score {
26 Score {
27 percent: if weight != 0 { 0.0 } else { 100.0 },
28 covered: 0,
29 total: 0,
30 contributes: false,
31 }
32}
33
34/// A coverpoint or cross measured by its own bins (19.11.1, 19.11.2).
35pub fn item_score(weight: i64, cov: Coverage) -> Score {
36 match cov.percent() {
37 Some(percent) => Score {
38 percent,
39 covered: cov.covered,
40 total: cov.total,
41 contributes: true,
42 },
43 None => empty_score(weight),
44 }
45}
46
47/// The weighted average `sum(Wi * Ci) / sum(Wi)` of `parts` (19.11), where
48/// `weight` is the weight of the thing being averaged (a covergroup, or a type)
49/// that decides the zero-denominator value.
50///
51/// Parts that do not contribute are left out entirely; when no contributing
52/// part has a nonzero weight the denominator is zero. `covered` and `total` are
53/// the plain sums over the contributing parts: 19.8 calls them "aggregated
54/// numbers of bins", so weights scale only the percentage.
55pub fn weighted_score(weight: i64, parts: &[(i64, Score)]) -> Score {
56 let mut num = 0.0f64;
57 let mut den = 0.0f64;
58 let mut covered = 0u64;
59 let mut total = 0u64;
60 for (w, s) in parts.iter().filter(|(_, s)| s.contributes) {
61 num += *w as f64 * s.percent;
62 den += *w as f64;
63 covered = covered.saturating_add(s.covered);
64 total = total.saturating_add(s.total);
65 }
66 if den == 0.0 {
67 return empty_score(weight);
68 }
69 Score {
70 percent: num / den,
71 covered,
72 total,
73 contributes: true,
74 }
75}
76
77/// The coverage of one item merged over instances (19.11.3,
78/// `type_option.merge_instances`). `views` holds, per instance, its `at_least`
79/// and its bins with hit counts. Bins are the same bin when their names are:
80/// the hit counts add up, and a bin is covered when the sum reaches the
81/// largest `at_least` of all instances (19.11.1).
82pub fn merge_bins(views: &[(u64, Vec<(String, u64)>)]) -> Coverage {
83 let at_least = views.iter().map(|(a, _)| *a).max().unwrap_or(1);
84 let mut union: BTreeMap<&str, u64> = BTreeMap::new();
85 for (_, bins) in views {
86 for (name, hits) in bins {
87 let h = union.entry(name.as_str()).or_insert(0);
88 *h = h.saturating_add(*hits);
89 }
90 }
91 Coverage {
92 covered: union.values().filter(|&&h| h >= at_least).count() as u64,
93 total: union.len() as u64,
94 }
95}