Skip to main content

score_set/
metric.rs

1use crate::float::Float;
2use crate::value::Value01;
3use core::marker::PhantomData;
4use witnessed::Witnessed;
5
6// ---------------------------------------------------------------------------
7// Metric builder — measure().by() → map01().by() → build()
8// ---------------------------------------------------------------------------
9
10/// Entry point: `metric("name")`.
11pub fn metric(name: &'static str) -> MetricName {
12    MetricName { name }
13}
14
15/// First stage: a name has been given, waiting for `.measure()`.
16pub struct MetricName {
17    name: &'static str,
18}
19
20impl MetricName {
21    /// Enter the measure stage.
22    pub fn measure(self) -> MeasureStage {
23        MeasureStage { name: self.name }
24    }
25}
26
27/// Second stage: ready for `.by(measure_closure)`.
28pub struct MeasureStage {
29    name: &'static str,
30}
31
32impl MeasureStage {
33    /// Supply the measure closure `Fn(I) -> Raw`.
34    pub fn by<I, Raw, M>(self, measure: M) -> MeasureSet<I, Raw, M> {
35        MeasureSet {
36            name: self.name,
37            measure,
38            _phantom: PhantomData,
39        }
40    }
41}
42
43/// Third stage: measure closure is set, waiting for `.map01()`.
44pub struct MeasureSet<I, Raw, M> {
45    name: &'static str,
46    measure: M,
47    _phantom: PhantomData<(I, Raw)>,
48}
49
50impl<I, Raw, M> MeasureSet<I, Raw, M> {
51    /// Enter the map01 stage.
52    pub fn map01(self) -> Map01Stage<I, Raw, M> {
53        Map01Stage {
54            name: self.name,
55            measure: self.measure,
56            _phantom: PhantomData,
57        }
58    }
59}
60
61/// Fourth stage: ready for `.by(map01_closure)`.
62pub struct Map01Stage<I, Raw, M> {
63    name: &'static str,
64    measure: M,
65    _phantom: PhantomData<(I, Raw)>,
66}
67
68impl<I, Raw, M> Map01Stage<I, Raw, M> {
69    /// Supply the map01 closure `Fn(&Raw, I) -> Witnessed<T, Value01>`.
70    ///
71    /// The type `T: Float` is inferred from the return type of the closure.
72    pub fn by<T: Float, F>(self, map01: F) -> Metric<T, I, Raw, M, F> {
73        Metric {
74            name: self.name,
75            measure: self.measure,
76            map01,
77            _phantom: PhantomData,
78        }
79    }
80}
81
82// Shape methods — available when Raw is the same float type as the Value01 output.
83
84impl<I, T: Float, M> Map01Stage<I, T, M> {
85    /// Identity: clamp raw value to `[0, 1]`.
86    pub fn identity(self) -> Metric<T, I, T, M, impl Fn(&T, &I) -> Witnessed<T, Value01>> {
87        self.by(|raw: &T, _: &I| Value01::witness(raw.min(T::one()).max(T::zero())).unwrap())
88    }
89
90    /// Linear: `raw / max`, clamped to `[0, 1]`.
91    pub fn linear(self, max: T) -> Metric<T, I, T, M, impl Fn(&T, &I) -> Witnessed<T, Value01>> {
92        self.by(move |raw: &T, _: &I| {
93            Value01::witness((*raw / max).min(T::one()).max(T::zero())).unwrap()
94        })
95    }
96
97    /// Sigmoid fitted to `[low, high]`.
98    ///
99    /// Automatically derives midpoint `x0 = (low + high) / 2` and steepness
100    /// `k = 2·ln(1/ε − 1) / (high − low)` where `ε = 10·epsilon`.
101    /// At `raw = low` output is ≈ ε, at `raw = high` ≈ 1−ε.
102    pub fn sigmoid(
103        self,
104        low: T,
105        high: T,
106    ) -> Metric<T, I, T, M, impl Fn(&T, &I) -> Witnessed<T, Value01>> {
107        let two = T::from_f64(2.0);
108        let eps = T::from_f64(10.0) * T::epsilon();
109        let x0 = (low + high) / two;
110        let k = two * (T::one() / eps - T::one()).ln() / (high - low);
111        self.by(move |raw: &T, _: &I| {
112            let v = T::one() / (T::one() + (-k * (*raw - x0)).exp());
113            Value01::witness(v).unwrap()
114        })
115    }
116
117    /// Asymmetric Cauchy: `1 / (1 + (raw / half)^2)`, with independent
118    /// half-widths for the left (`raw < 0`) and right (`raw >= 0`) sides.
119    pub fn cauchy(self, left: T, right: T) -> Metric<T, I, T, M, impl Fn(&T, &I) -> Witnessed<T, Value01>>
120    {
121        self.by(move |raw: &T, _: &I| {
122            let h = if *raw < T::zero() { left } else { right };
123            let v = T::one() / (T::one() + (*raw / h) * (*raw / h));
124            Value01::witness(v).unwrap()
125        })
126    }
127}
128
129// ---------------------------------------------------------------------------
130// Metric — the built scoring operator
131// ---------------------------------------------------------------------------
132
133/// A scoring operator built via the `measure().by() → map01().by()` pipeline.
134///
135/// Stores two closures:
136/// - `measure: Fn(I) -> Raw` — maps input to an intermediate raw value.
137/// - `map01: Fn(&Raw, I) -> Witnessed<T, Value01>` — maps the raw value back
138///   alongside the original input to a validated `[0, 1]` score.
139///
140/// `eval(input)` composes the two closures.
141pub struct Metric<T, I, Raw, M, F> {
142    pub(crate) name: &'static str,
143    pub(crate) measure: M,
144    pub(crate) map01: F,
145    _phantom: PhantomData<(T, I, Raw)>,
146}
147
148impl<T: Float, I, Raw, M, F> Metric<T, I, Raw, M, F>
149where
150    M: Fn(&I) -> Raw,
151    F: Fn(&Raw, &I) -> Witnessed<T, Value01>,
152{
153    /// Evaluate this metric against an input, producing a `[0, 1]` score.
154    #[inline]
155    pub fn eval(&self, input: &I) -> Witnessed<T, Value01> {
156        let raw = (self.measure)(input);
157        (self.map01)(&raw, input)
158    }
159
160    /// Return the metric's name.
161    #[inline]
162    pub fn name(&self) -> &str {
163        self.name
164    }
165}
166
167// Make Metric clone-able when closures are clone-able.
168impl<T, I, Raw, M: Clone, F: Clone> Clone for Metric<T, I, Raw, M, F> {
169    fn clone(&self) -> Self {
170        Self {
171            name: self.name,
172            measure: self.measure.clone(),
173            map01: self.map01.clone(),
174            _phantom: PhantomData,
175        }
176    }
177}
178
179#[cfg(test)]
180mod tests_for_metric;