score-set 2.1.0

A Rust library for building static weighted scoring operator sets
Documentation
use score_set::{
    DynScoreSet32, DynScoreSet64, Metric32, Metric64,
    traits::{EvalF32, EvalF64, Map01F32, Map01F64, Measure, V01, prove_v01_f32, prove_v01_f64},
};
use witnessed::{WitnessExt, Witnessed};

mod f64_tests {
    use super::*;

    #[derive(Clone, Copy)]
    struct Context {
        latency_ms: f64,
        cpu_usage: f64,
    }

    struct Latency;

    impl Measure<Context> for Latency {
        type Output = f64;

        fn measure(&self, ctx: &Context) -> Self::Output {
            ctx.latency_ms
        }
    }

    struct CpuUsage;

    impl Measure<Context> for CpuUsage {
        type Output = f64;

        fn measure(&self, ctx: &Context) -> Self::Output {
            ctx.cpu_usage
        }
    }

    struct LowerIsBetter {
        limit: f64,
    }

    impl Map01F64 for LowerIsBetter {
        type Input = f64;

        fn map(&self, value: Self::Input) -> Witnessed<f64, V01> {
            let value = (1.0 - value / self.limit).clamp(0.0, 1.0);
            value
                .witness()
                .by(prove_v01_f64)
                .expect("value was clamped")
        }
    }

    struct Identity;

    impl Map01F64 for Identity {
        type Input = f64;

        fn map(&self, value: Self::Input) -> Witnessed<f64, V01> {
            let value = value.clamp(0.0, 1.0);
            value
                .witness()
                .by(prove_v01_f64)
                .expect("value was clamped")
        }
    }

    #[test]
    fn metric_composes_measure_map_and_weight() {
        let metric = Metric64::new(Latency, LowerIsBetter { limit: 100.0 }, 0.7);

        let ctx = Context {
            latency_ms: 40.0,
            cpu_usage: 0.25,
        };

        // 0.7 * (1 - 40 / 100) = 0.42
        assert!((metric.eval(&ctx) - 0.42).abs() < 1e-12);
    }

    #[test]
    fn dyn_score_set_sums_heterogeneous_metrics() {
        let score_set = DynScoreSet64::<Context>::builder()
            .append(Metric64::new(Latency, LowerIsBetter { limit: 100.0 }, 0.7))
            .append(Metric64::new(CpuUsage, Identity, 0.3))
            .build();

        let cases = [
            (
                Context {
                    latency_ms: 40.0,
                    cpu_usage: 0.25,
                },
                0.495,
            ),
            (
                Context {
                    latency_ms: 0.0,
                    cpu_usage: 1.0,
                },
                1.0,
            ),
            (
                Context {
                    latency_ms: 200.0,
                    cpu_usage: -1.0,
                },
                0.0,
            ),
        ];

        for (ctx, expected) in cases {
            let actual = score_set.eval(&ctx);

            assert!(
                (actual - expected).abs() < 1e-12,
                "expected {expected}, got {actual}",
            );
        }
    }

    #[test]
    fn empty_dyn_score_set_evaluates_to_zero() {
        let score_set = DynScoreSet64::<Context>::builder().build();

        let ctx = Context {
            latency_ms: 40.0,
            cpu_usage: 0.25,
        };

        assert_eq!(score_set.eval(&ctx), 0.0);
    }
}

mod f32_tests {
    use super::*;

    #[derive(Clone, Copy)]
    struct Context {
        latency_ms: f32,
        cpu_usage: f32,
    }

    struct Latency;

    impl Measure<Context> for Latency {
        type Output = f32;

        fn measure(&self, ctx: &Context) -> Self::Output {
            ctx.latency_ms
        }
    }

    struct CpuUsage;

    impl Measure<Context> for CpuUsage {
        type Output = f32;

        fn measure(&self, ctx: &Context) -> Self::Output {
            ctx.cpu_usage
        }
    }

    struct LowerIsBetter {
        limit: f32,
    }

    impl Map01F32 for LowerIsBetter {
        type Input = f32;

        fn map(&self, value: Self::Input) -> Witnessed<f32, V01> {
            let value = (1.0 - value / self.limit).clamp(0.0, 1.0);
            value
                .witness()
                .by(prove_v01_f32)
                .expect("value was clamped")
        }
    }

    struct Identity;

    impl Map01F32 for Identity {
        type Input = f32;

        fn map(&self, value: Self::Input) -> Witnessed<f32, V01> {
            let value = value.clamp(0.0, 1.0);
            value
                .witness()
                .by(prove_v01_f32)
                .expect("value was clamped")
        }
    }

    #[test]
    fn metric_composes_measure_map_and_weight() {
        let metric = Metric32::new(Latency, LowerIsBetter { limit: 100.0 }, 0.7);

        let ctx = Context {
            latency_ms: 40.0,
            cpu_usage: 0.25,
        };

        // 0.7 * (1 - 40 / 100) = 0.42
        assert!((metric.eval(&ctx) - 0.42).abs() < 1e-6);
    }

    #[test]
    fn dyn_score_set_sums_heterogeneous_metrics() {
        let score_set = DynScoreSet32::<Context>::builder()
            .append(Metric32::new(Latency, LowerIsBetter { limit: 100.0 }, 0.7))
            .append(Metric32::new(CpuUsage, Identity, 0.3))
            .build();

        let cases = [
            (
                Context {
                    latency_ms: 40.0,
                    cpu_usage: 0.25,
                },
                0.495,
            ),
            (
                Context {
                    latency_ms: 0.0,
                    cpu_usage: 1.0,
                },
                1.0,
            ),
            (
                Context {
                    latency_ms: 200.0,
                    cpu_usage: -1.0,
                },
                0.0,
            ),
        ];

        for (ctx, expected) in cases {
            let actual = score_set.eval(&ctx);

            assert!(
                (actual - expected).abs() < 1e-6,
                "expected {expected}, got {actual}",
            );
        }
    }

    #[test]
    fn empty_dyn_score_set_evaluates_to_zero() {
        let score_set = DynScoreSet32::<Context>::builder().build();

        let ctx = Context {
            latency_ms: 40.0,
            cpu_usage: 0.25,
        };

        assert_eq!(score_set.eval(&ctx), 0.0);
    }
}

#[test]
fn prove_v01_accepts_both_float_types_and_rejects_invalid_values() {
    assert!(0.0_f32.witness().by(prove_v01_f32).is_ok());
    assert!(1.0_f64.witness().by(prove_v01_f64).is_ok());
    assert!((-0.1_f32).witness().by(prove_v01_f32).is_err());
    assert!(1.1_f64.witness().by(prove_v01_f64).is_err());
    assert!(f32::NAN.witness().by(prove_v01_f32).is_err());
}