#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Verdict {
Accepted(f32),
Rejected(Reject),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Reject {
NonFinite,
OutOfBounds,
Degenerate,
JudgeVeto,
}
pub trait IntentJudge {
fn veto(&self, fitness: f32) -> bool;
}
#[derive(Clone, Copy, Debug, Default)]
pub struct NoJudge;
impl IntentJudge for NoJudge {
fn veto(&self, _fitness: f32) -> bool {
false
}
}
#[derive(Clone, Copy, Debug)]
pub struct Guard<J: IntentJudge = NoJudge> {
judge: J,
}
impl Guard<NoJudge> {
#[must_use]
pub fn deterministic() -> Self {
Self { judge: NoJudge }
}
}
impl<J: IntentJudge> Guard<J> {
pub fn with_judge(judge: J) -> Self {
Self { judge }
}
pub fn screen(
&self,
fitness: f32,
finite_metrics: bool,
in_bounds: bool,
degenerate: bool,
) -> Verdict {
if !finite_metrics || !fitness.is_finite() {
return Verdict::Rejected(Reject::NonFinite);
}
if !in_bounds {
return Verdict::Rejected(Reject::OutOfBounds);
}
if degenerate {
return Verdict::Rejected(Reject::Degenerate);
}
if self.judge.veto(fitness) {
return Verdict::Rejected(Reject::JudgeVeto);
}
Verdict::Accepted(fitness)
}
}
#[must_use]
pub fn best_accepted(verdicts: &[Verdict]) -> Option<(usize, f32)> {
verdicts
.iter()
.enumerate()
.filter_map(|(i, v)| match v {
Verdict::Accepted(f) => Some((i, *f)),
Verdict::Rejected(_) => None,
})
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
}
#[must_use]
pub fn reject_summary(verdicts: &[Verdict]) -> [usize; 4] {
let mut c = [0usize; 4];
for v in verdicts {
if let Verdict::Rejected(r) = v {
c[match r {
Reject::NonFinite => 0,
Reject::OutOfBounds => 1,
Reject::Degenerate => 2,
Reject::JudgeVeto => 3,
}] += 1;
}
}
c
}
use std::collections::HashSet;
#[must_use]
pub fn contamination<'a>(
train_ids: impl IntoIterator<Item = &'a str>,
eval_holdout: &[&str],
) -> Vec<String> {
let holdout: HashSet<&str> = eval_holdout.iter().copied().collect();
let mut bad: Vec<String> = train_ids
.into_iter()
.filter(|id| holdout.contains(id))
.map(str::to_string)
.collect();
bad.sort();
bad.dedup();
bad
}
pub fn assert_train_eval_disjoint(
train_ids: &[&str],
eval_holdout: &[&str],
) -> Result<(), Vec<String>> {
let bad = contamination(train_ids.iter().copied(), eval_holdout);
if bad.is_empty() {
Ok(())
} else {
Err(bad)
}
}
pub fn filter_holdout<T>(
items: Vec<T>,
id_of: impl Fn(&T) -> &str,
eval_holdout: &[&str],
) -> (Vec<T>, Vec<T>) {
let holdout: HashSet<&str> = eval_holdout.iter().copied().collect();
let mut kept = Vec::new();
let mut excluded = Vec::new();
for it in items {
if holdout.contains(id_of(&it)) {
excluded.push(it);
} else {
kept.push(it);
}
}
(kept, excluded)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn non_finite_is_excluded_not_zeroed() {
let g = Guard::deterministic();
assert_eq!(
g.screen(f32::NAN, true, true, false),
Verdict::Rejected(Reject::NonFinite)
);
assert_eq!(
g.screen(1.0, false, true, false),
Verdict::Rejected(Reject::NonFinite)
);
}
#[test]
fn out_of_bounds_and_degenerate_rejected() {
let g = Guard::deterministic();
assert_eq!(
g.screen(5.0, true, false, false),
Verdict::Rejected(Reject::OutOfBounds)
);
assert_eq!(
g.screen(5.0, true, true, true),
Verdict::Rejected(Reject::Degenerate)
);
}
#[test]
fn best_accepted_excludes_rejects_and_is_nan_safe() {
let vs = [
Verdict::Accepted(-0.5),
Verdict::Rejected(Reject::NonFinite),
Verdict::Accepted(-0.2),
Verdict::Rejected(Reject::Degenerate),
];
assert_eq!(best_accepted(&vs), Some((2, -0.2)));
assert_eq!(reject_summary(&vs), [1, 0, 1, 0]);
assert_eq!(
best_accepted(&[Verdict::Rejected(Reject::OutOfBounds)]),
None
);
}
#[test]
fn judge_vetoes_but_does_not_set_reward() {
struct VetoHigh;
impl IntentJudge for VetoHigh {
fn veto(&self, fitness: f32) -> bool {
fitness > 100.0 }
}
let g = Guard::with_judge(VetoHigh);
assert_eq!(
g.screen(999.0, true, true, false),
Verdict::Rejected(Reject::JudgeVeto)
);
assert_eq!(g.screen(1.0, true, true, false), Verdict::Accepted(1.0));
}
#[test]
fn disjoint_train_eval_ok_and_contamination_detected() {
let eval = ["i-3", "i-9"];
assert_eq!(assert_train_eval_disjoint(&["i-1", "i-2"], &eval), Ok(()));
assert_eq!(
assert_train_eval_disjoint(&["i-1", "i-9", "i-3", "i-9"], &eval),
Err(vec!["i-3".to_string(), "i-9".to_string()])
);
}
#[test]
fn filter_holdout_partitions_by_id() {
let items = vec![("i-1", 10), ("i-3", 20), ("i-5", 30)];
let (kept, excluded) = filter_holdout(items, |x| x.0, &["i-3"]);
assert_eq!(
kept.iter().map(|x| x.0).collect::<Vec<_>>(),
vec!["i-1", "i-5"]
);
assert_eq!(
excluded.iter().map(|x| x.0).collect::<Vec<_>>(),
vec!["i-3"]
);
let kept_ids: Vec<&str> = kept.iter().map(|x| x.0).collect();
assert!(assert_train_eval_disjoint(&kept_ids, &["i-3"]).is_ok());
}
}