use std::collections::BTreeMap;
use rustev_contract::Document;
use rustev_contract::decimal::Decimal;
use rustev_contract::eval_report::MetricValue;
use rustev_contract::ids::ContentDigest;
use rustev_contract::limits::REPLAY_V1;
use serde::{Deserialize, Serialize};
use crate::repeat_config::TopK;
use crate::repeat_stats::{Contribution, Figure, Form, MetricCases, run_stats};
use crate::report::Evaluated;
pub const EVAL_CASES: &str = "rustev.eval-cases/1";
pub const COST_PER_1000_OBSERVED: &str = "cost_per_1000/observed";
pub const COST_PER_1000_ESTIMATED: &str = "cost_per_1000/estimated";
pub fn derived(metric: &str) -> bool {
metric == COST_PER_1000_OBSERVED || metric == COST_PER_1000_ESTIMATED
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EvalCases {
pub schema: String,
pub report: ContentDigest,
pub top_k: Option<TopK>,
pub cases: Vec<CaseMeasures>,
}
crate::doc::eval_document!(EvalCases, EVAL_CASES, REPLAY_V1);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CaseMeasures {
pub case: String,
pub run: Option<ContentDigest>,
pub outcome: String,
pub top_k: Option<Vec<String>>,
pub measures: BTreeMap<String, CaseMeasure>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum CaseMeasure {
Count {
num: u64,
den: u64,
},
Mean {
num: f64,
den: u64,
},
Sample {
value: f64,
},
Unknown {
reason: String,
},
}
impl CaseMeasure {
pub fn contribution(&self) -> Contribution {
match self {
CaseMeasure::Count { num, den } => Contribution::Count {
num: *num,
den: *den,
},
CaseMeasure::Mean { num, den } => Contribution::Mean {
num: *num,
den: *den,
},
CaseMeasure::Sample { value } => Contribution::Sample(*value),
CaseMeasure::Unknown { .. } => Contribution::Unknown,
}
}
}
pub fn form_of(metric: &str) -> Option<Form> {
match metric {
"coverage"
| "acceptance_coverage"
| "labeled_proposal_coverage"
| "error_among_accepted"
| "candidate_outcome_change" => return Some(Form::CountRatio),
"mean_log_loss" | COST_PER_1000_OBSERVED | COST_PER_1000_ESTIMATED => {
return Some(Form::Mean);
}
"brier" => return None,
_ => {}
}
let parts: Vec<&str> = metric.split('/').collect();
match parts.as_slice() {
["reliability", _, _, "confidence"] => Some(Form::Mean),
["reliability", _, _, "accuracy"] => Some(Form::CountRatio),
["ranking", _, spec] => {
if ["hit@", "short@", "backfill@p"]
.iter()
.any(|p| spec.starts_with(p))
{
Some(Form::CountRatio)
} else {
Some(Form::Mean)
}
}
["origin", ..] => Some(Form::CountRatio),
["agreement", _, "exact" | "adjacent"]
| ["model", "agreement", _, "exact" | "adjacent"] => Some(Form::CountRatio),
["agreement", _, "mean_abs_diff"] | ["model", "agreement", _, "mean_abs_diff"] => {
Some(Form::Mean)
}
["latency_ms" | "queue_ms", q] => {
let q = Decimal::parse(q.strip_prefix('p')?).ok()?;
Some(Form::Sample { quantile: q })
}
_ => None,
}
}
pub fn form_in(metric: &str, runs: &[&EvalCases]) -> Option<Form> {
if let Some(form) = form_of(metric) {
return Some(form);
}
let by_measures = ["constraint/", "rubric/", "model/rubric/"];
if !by_measures.iter().any(|p| metric.starts_with(p)) {
return None;
}
runs.iter()
.flat_map(|r| &r.cases)
.find_map(|c| match c.measures.get(metric)? {
CaseMeasure::Count { .. } => Some(Form::CountRatio),
CaseMeasure::Mean { .. } => Some(Form::Mean),
CaseMeasure::Sample { .. } | CaseMeasure::Unknown { .. } => None,
})
}
pub fn metric_cases(metric: &str, form: Form, runs: &[&EvalCases]) -> Result<MetricCases, String> {
let first = runs.first().map_or(&[][..], |r| r.cases.as_slice());
for r in runs {
if r.cases.len() != first.len() || r.cases.iter().zip(first).any(|(a, b)| a.case != b.case)
{
return Err("mixed-runs: the runs do not list the same cases in the same order".into());
}
}
let cases = (0..first.len())
.map(|c| {
runs.iter()
.map(|r| {
r.cases[c]
.measures
.get(metric)
.map(CaseMeasure::contribution)
})
.collect()
})
.collect();
Ok(MetricCases {
name: metric.into(),
form,
cases,
})
}
pub fn reconcile(run: &Evaluated, cases: &EvalCases) -> Result<(), String> {
let digest = run
.report
.record_digest()
.map_err(|_| "the report has no record digest".to_string())?;
if cases.schema != EVAL_CASES || cases.report != digest || run.detail.report != digest {
return Err("the case measures are not bound to this report".into());
}
if cases.cases.len() != run.detail.cases.len()
|| cases
.cases
.iter()
.zip(&run.detail.cases)
.any(|(m, d)| m.case != d.case || m.outcome != d.outcome)
{
return Err("the case measures do not list the detail's cases and outcomes".into());
}
for c in &cases.cases {
if let Some(name) = c.measures.keys().find(|m| form_in(m, &[cases]).is_none()) {
return Err(format!(
"case {} has a measure for `{name}`, which has none",
c.case
));
}
}
for m in &run.report.metrics {
let Some(form) = form_in(&m.name, &[cases]) else {
continue;
};
let metric = metric_cases(&m.name, form, &[cases])?;
let stats = run_stats(&metric, 1, false).map_err(|e| format!("`{}`: {e}", m.name))?;
let same = match (&stats.runs[0], &m.value) {
(Figure::Known(a), MetricValue::Measured { value, .. }) => {
a.to_bits() == value.to_bits()
}
(Figure::Unknown { .. }, MetricValue::Unknown { .. }) => true,
_ => false,
};
if !same {
return Err(format!(
"`{}` computed from the case measures is not the report's value",
m.name
));
}
if let (Form::CountRatio, Some(count)) = (&metric.form, run.detail.count(&m.name)) {
let (mut num, mut den) = (0u128, 0u128);
for per_run in &metric.cases {
if let Some(Contribution::Count { num: n, den: d }) = per_run[0] {
num += u128::from(n);
den += u128::from(d);
}
}
if (num, den) != (u128::from(count.numerator), u128::from(count.denominator)) {
return Err(format!("`{}` sums are not the detail's counts", m.name));
}
}
}
for c in &cases.cases {
if let Some(name) = c
.measures
.keys()
.find(|n| !derived(n) && !run.report.metrics.iter().any(|m| &m.name == *n))
{
return Err(format!(
"case {} has a measure for unreported `{name}`",
c.case
));
}
}
Ok(())
}