vj-sim 0.3.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
//! Coverage computation (IEEE 1800-2023 19.11): the weighted average, the
//! zero-denominator rules and the merge of instances by bin name.

use std::collections::BTreeMap;

use super::coverpoint::Coverage;

/// The result of one `get_coverage` / `get_inst_coverage` query.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Score {
    /// The returned coverage number, 0.0 to 100.0.
    pub percent: f64,
    /// The `covered` output of the two-argument forms.
    pub covered: u64,
    /// The `total` output of the two-argument forms.
    pub total: u64,
    /// The item has a nonzero denominator: it takes part in its parent's
    /// weighted average. Otherwise it is excluded from numerator and
    /// denominator alike (19.11).
    pub contributes: bool,
}

/// What a query returns when its denominator is zero (19.11, rules b and c):
/// 0.0 for a nonzero weight, 100.0 for a zero weight; both outputs 0 (rule d).
pub fn empty_score(weight: i64) -> Score {
    Score {
        percent: if weight != 0 { 0.0 } else { 100.0 },
        covered: 0,
        total: 0,
        contributes: false,
    }
}

/// A coverpoint or cross measured by its own bins (19.11.1, 19.11.2).
pub fn item_score(weight: i64, cov: Coverage) -> Score {
    match cov.percent() {
        Some(percent) => Score {
            percent,
            covered: cov.covered,
            total: cov.total,
            contributes: true,
        },
        None => empty_score(weight),
    }
}

/// The weighted average `sum(Wi * Ci) / sum(Wi)` of `parts` (19.11), where
/// `weight` is the weight of the thing being averaged (a covergroup, or a type)
/// that decides the zero-denominator value.
///
/// Parts that do not contribute are left out entirely; when no contributing
/// part has a nonzero weight the denominator is zero. `covered` and `total` are
/// the plain sums over the contributing parts: 19.8 calls them "aggregated
/// numbers of bins", so weights scale only the percentage.
pub fn weighted_score(weight: i64, parts: &[(i64, Score)]) -> Score {
    let mut num = 0.0f64;
    let mut den = 0.0f64;
    let mut covered = 0u64;
    let mut total = 0u64;
    for (w, s) in parts.iter().filter(|(_, s)| s.contributes) {
        num += *w as f64 * s.percent;
        den += *w as f64;
        covered = covered.saturating_add(s.covered);
        total = total.saturating_add(s.total);
    }
    if den == 0.0 {
        return empty_score(weight);
    }
    Score {
        percent: num / den,
        covered,
        total,
        contributes: true,
    }
}

/// The coverage of one item merged over instances (19.11.3,
/// `type_option.merge_instances`). `views` holds, per instance, its `at_least`
/// and its bins with hit counts. Bins are the same bin when their names are:
/// the hit counts add up, and a bin is covered when the sum reaches the
/// largest `at_least` of all instances (19.11.1).
pub fn merge_bins(views: &[(u64, Vec<(String, u64)>)]) -> Coverage {
    let at_least = views.iter().map(|(a, _)| *a).max().unwrap_or(1);
    let mut union: BTreeMap<&str, u64> = BTreeMap::new();
    for (_, bins) in views {
        for (name, hits) in bins {
            let h = union.entry(name.as_str()).or_insert(0);
            *h = h.saturating_add(*hits);
        }
    }
    Coverage {
        covered: union.values().filter(|&&h| h >= at_least).count() as u64,
        total: union.len() as u64,
    }
}