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(
120        self,
121        left: T,
122        right: T,
123    ) -> Metric<T, I, T, M, impl Fn(&T, &I) -> Witnessed<T, Value01>> {
124        self.by(move |raw: &T, _: &I| {
125            let h = if *raw < T::zero() { left } else { right };
126            let v = T::one() / (T::one() + (*raw / h) * (*raw / h));
127            Value01::witness(v).unwrap()
128        })
129    }
130}
131
132// ---------------------------------------------------------------------------
133// Metric — the built scoring operator
134// ---------------------------------------------------------------------------
135
136/// A scoring operator built via the `measure().by() → map01().by()` pipeline.
137///
138/// Stores two closures:
139/// - `measure: Fn(I) -> Raw` — maps input to an intermediate raw value.
140/// - `map01: Fn(&Raw, I) -> Witnessed<T, Value01>` — maps the raw value back
141///   alongside the original input to a validated `[0, 1]` score.
142///
143/// `eval(input)` composes the two closures.
144pub struct Metric<T, I, Raw, M, F> {
145    pub(crate) name: &'static str,
146    pub(crate) measure: M,
147    pub(crate) map01: F,
148    _phantom: PhantomData<(T, I, Raw)>,
149}
150
151impl<T: Float, I, Raw, M, F> Metric<T, I, Raw, M, F>
152where
153    M: Fn(&I) -> Raw,
154    F: Fn(&Raw, &I) -> Witnessed<T, Value01>,
155{
156    /// Evaluate this metric against an input, producing a `[0, 1]` score.
157    #[inline]
158    pub fn eval(&self, input: &I) -> Witnessed<T, Value01> {
159        let raw = (self.measure)(input);
160        (self.map01)(&raw, input)
161    }
162
163    /// Return the metric's name.
164    #[inline]
165    pub fn name(&self) -> &str {
166        self.name
167    }
168}
169
170// Separate impl block for .boxed() — requires 'static for Box<dyn>.
171impl<T: Float, I: 'static, Raw: 'static, M: 'static, F: 'static> Metric<T, I, Raw, M, F>
172where
173    M: Fn(&I) -> Raw,
174    F: Fn(&Raw, &I) -> Witnessed<T, Value01>,
175{
176    /// Box this metric as a `Box<dyn Scorable<T, I>>` for use in
177    /// [`DynamicScoreSet`](crate::DynamicScoreSet).
178    ///
179    /// This is a convenience over `Box::new(metric)` — it infers the
180    /// `Scorable` type automatically, avoiding an explicit annotation.
181    #[inline]
182    pub fn boxed(self) -> Box<dyn crate::Scorable<T, I>> {
183        Box::new(self)
184    }
185}
186
187// Make Metric clone-able when closures are clone-able.
188impl<T, I, Raw, M: Clone, F: Clone> Clone for Metric<T, I, Raw, M, F> {
189    fn clone(&self) -> Self {
190        Self {
191            name: self.name,
192            measure: self.measure.clone(),
193            map01: self.map01.clone(),
194            _phantom: PhantomData,
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests_for_metric;