use std::collections::BTreeSet;
use std::fmt;
use rustev_contract::bounded::ParseLimits;
use rustev_contract::canonical::tagged_digest;
use rustev_contract::decimal::Decimal;
use rustev_contract::ids::{ContentDigest, EvaluatorConfigId};
use rustev_contract::limits::REPLAY_V1;
use rustev_contract::{Document, DocumentError, Identified};
use serde::{Deserialize, Serialize};
use crate::dataset::AdapterRef;
pub const EVALUATOR_CONFIG: &str = "rustev.evaluator-config/1";
pub const EVALUATOR_CONFIG_V2: &str = "rustev.evaluator-config/2";
pub const MAX_GRID: usize = 4096;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EvaluatorConfig {
pub schema: String,
pub adapter: ConfigAdapter,
pub label_interpretation: String,
pub agreement: Agreement,
pub probability_step: Option<String>,
pub reliability_bins: Vec<Decimal>,
pub subgroups: Vec<String>,
pub latency_quantiles: Vec<Decimal>,
pub cost_units_comparable: bool,
pub metrics: Vec<MetricFormula>,
pub gates: Vec<Gate>,
pub temperature_grid: Vec<Decimal>,
}
impl Document for EvaluatorConfig {
const SCHEMA: &'static str = EVALUATOR_CONFIG;
const LIMITS: ParseLimits = REPLAY_V1;
fn schema(&self) -> &str {
&self.schema
}
fn parse(bytes: &[u8]) -> Result<Self, DocumentError> {
let doc: Self = rustev_contract::bounded::parse_bounded(bytes, &Self::LIMITS)
.map_err(DocumentError::Parse)?;
if doc.schema != EVALUATOR_CONFIG && doc.schema != EVALUATOR_CONFIG_V2 {
return Err(DocumentError::Schema {
expected: EVALUATOR_CONFIG_V2,
found: doc.schema.chars().take(80).collect(),
});
}
Ok(doc)
}
fn record_digest(&self) -> Result<ContentDigest, rustev_contract::canonical::CanonicalError> {
let raw = self.record_canonical()?;
Ok(ContentDigest::parse(&tagged_digest(&self.schema, &raw))
.expect("a tagged digest is a content digest"))
}
}
impl Identified for EvaluatorConfig {
type Id = EvaluatorConfigId;
fn wrap(digest: String) -> EvaluatorConfigId {
EvaluatorConfigId::parse(&digest).expect("a tagged digest is an identity")
}
fn id(&self) -> Result<EvaluatorConfigId, rustev_contract::canonical::CanonicalError> {
Ok(Self::wrap(tagged_digest(&self.schema, &self.canonical()?)))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ConfigAdapter {
pub name: String,
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rules: Option<AdapterRules>,
}
impl ConfigAdapter {
pub fn v1(r: AdapterRef) -> ConfigAdapter {
ConfigAdapter {
name: r.name,
version: r.version,
rules: None,
}
}
pub fn v2(r: AdapterRef, rules: AdapterRules) -> ConfigAdapter {
ConfigAdapter {
name: r.name,
version: r.version,
rules: Some(rules),
}
}
pub fn reference(&self) -> AdapterRef {
AdapterRef {
name: self.name.clone(),
version: self.version.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AdapterRules {
Bound(ContentDigest),
Opaque,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Agreement {
pub params: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MetricFormula {
pub name: String,
pub formula: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Direction {
LowerIsBetter,
HigherIsBetter,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Gate {
pub name: String,
pub metric: String,
pub direction: Direction,
pub tolerance: Decimal,
pub min_comparable_coverage: Decimal,
pub min_labeled_coverage: Decimal,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigError(pub String);
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for ConfigError {}
fn increasing(v: &[Decimal]) -> bool {
v.windows(2).all(|w| w[0] < w[1])
}
impl EvaluatorConfig {
pub fn bound_rules(&self) -> Option<&ContentDigest> {
match (&self.adapter.rules, self.schema.as_str()) {
(Some(AdapterRules::Bound(d)), EVALUATOR_CONFIG_V2) => Some(d),
_ => None,
}
}
pub fn check(&self) -> Result<(), ConfigError> {
let bad = |m: &str| Err(ConfigError(m.into()));
match (self.schema.as_str(), &self.adapter.rules) {
(EVALUATOR_CONFIG, None) | (EVALUATOR_CONFIG_V2, Some(_)) => {}
(EVALUATOR_CONFIG, Some(_)) => return bad("version 1 names no adapter rules"),
(EVALUATOR_CONFIG_V2, None) => return bad("version 2 names the adapter rules"),
_ => return bad("schema"),
}
let one = Decimal::from_i64(1);
let b = &self.reliability_bins;
if b.len() < 2 || b[0] != Decimal::ZERO || b[b.len() - 1] != one || !increasing(b) {
return bad("reliability bins run strictly upward from 0 to 1");
}
let q = &self.latency_quantiles;
if q.iter().any(|x| *x <= Decimal::ZERO || *x > one) || !increasing(q) {
return bad("latency quantiles are strictly increasing in (0, 1]");
}
let names: BTreeSet<&str> = self.metrics.iter().map(|m| m.name.as_str()).collect();
if names.len() != self.metrics.len() {
return bad("metric names are unique");
}
let groups: BTreeSet<&str> = self.subgroups.iter().map(String::as_str).collect();
if groups.len() != self.subgroups.len() || groups.contains("all") {
return bad("subgroups are unique and none is named `all`");
}
let mut gate_names = BTreeSet::new();
for g in &self.gates {
if !gate_names.insert(&g.name) {
return bad("gate names are unique");
}
let base = g.metric.split('/').next().unwrap_or_default();
if !names.contains(base) {
return bad("a gate names a metric the configuration does not declare");
}
if g.tolerance.is_negative() {
return bad("a gate tolerance is non-negative");
}
for c in [g.min_comparable_coverage, g.min_labeled_coverage] {
if c.is_negative() || c > one {
return bad("a minimum coverage is in [0, 1]");
}
}
}
let t = &self.temperature_grid;
let max = Decimal::from_i64(1000);
if t.len() > MAX_GRID || !increasing(t) || t.iter().any(|x| *x <= Decimal::ZERO || *x > max)
{
return bad("the temperature grid is strictly increasing in (0, 1000], at most 4096");
}
Ok(())
}
}