Skip to main content

score_set/
finite.rs

1// ===========================================================================
2// finite_metric! — generate a zero-vtable finite metric enum (Layer 2)
3// ===========================================================================
4
5/// Declare a finite enum of metric types with static-dispatch `eval`.
6///
7/// This macro generates an enum whose variants each wrap a concrete metric
8/// type. The generated [`Scorable`](crate::Scorable) implementation uses
9/// `match` + static dispatch — zero vtable overhead.
10///
11/// # Syntax — concrete form
12///
13/// `float` and `subject` are concrete types of the scoring domain:
14///
15/// ```ignore
16/// finite_metric! {
17///     metric     => DnaKind,
18///     float      => f64,
19///     subject    => DnaContext<'static>,
20///     dimensions =>
21///         Gc(GcRatio),
22///         Len(SeqLen),
23/// }
24/// ```
25///
26/// # Syntax — generic form
27///
28/// `T, I` in angle brackets on the metric name declare type parameters.
29/// No separate `float`/`subject` keys needed — the names appear in `<T, I>`:
30///
31/// ```ignore
32/// finite_metric! {
33///     pub metric => MetricKind<T, I>,
34///     dimensions =>
35///         Gc(GcRatio<T, I>),
36///         Tm(TmScore<T, I>),
37///     }
38/// }
39/// ```
40///
41/// # Generated items
42///
43/// - An enum with the listed variants.
44/// - A [`Scorable`](crate::Scorable) implementation with
45///   static-dispatch `eval` and `name` methods.
46///
47/// # Requirements on variant types
48///
49/// Each variant's inner type must provide:
50/// - `fn eval(&self, input: &I) -> Witnessed<T, Value01>`
51/// - `fn name(&self) -> &str`
52///
53/// Both [`Metric`](crate::Metric) and `Box<dyn Scorable<T, I>>` satisfy
54/// this contract.
55#[macro_export]
56macro_rules! finite_metric {
57    // ---- concrete: metric => Name, float => f64, subject => DnaContext ----
58    (
59        $(#[$attr:meta])*
60        $vis:vis
61        metric => $name:ident,
62        float => $T:ty,
63        subject => $I:ty,
64        dimensions => $($variant:ident($ty:ty)),+ $(,)?
65    ) => {
66        $(#[$attr])*
67        #[allow(clippy::pub_enum_variant_fields)]
68        $vis enum $name {
69            $($variant($ty),)+
70        }
71
72        impl $crate::Scorable<$T, $I> for $name {
73            #[inline]
74            fn eval(&self, input: &$I) -> $crate::Witnessed<$T, $crate::Value01> {
75                match self {
76                    $(Self::$variant(m) => m.eval(input)),+
77                }
78            }
79
80            #[inline]
81            fn name(&self) -> &str {
82                match self {
83                    $(Self::$variant(m) => m.name()),+
84                }
85            }
86        }
87    };
88
89    // ---- generic: metric => Name<T, I>, dimensions => ... ----
90    (
91        $(#[$attr:meta])*
92        $vis:vis
93        metric => $name:ident<$T:ident, $I:ident>,
94        dimensions => $($variant:ident($ty:ty)),+ $(,)?
95    ) => {
96        $(#[$attr])*
97        #[allow(clippy::pub_enum_variant_fields)]
98        $vis enum $name<$T: $crate::Float, $I> {
99            $($variant($ty)),+
100        }
101
102        impl<$T: $crate::Float, $I> $crate::Scorable<$T, $I> for $name<$T, $I> {
103            #[inline]
104            fn eval(&self, input: &$I) -> $crate::Witnessed<$T, $crate::Value01> {
105                match self {
106                    $(Self::$variant(m) => m.eval(input)),+
107                }
108            }
109
110            #[inline]
111            fn name(&self) -> &str {
112                match self {
113                    $(Self::$variant(m) => m.name()),+
114                }
115            }
116        }
117    };
118}
119
120// ===========================================================================
121// FiniteScoreSet — weighted set with finite-enum dispatch (Layer 2)
122// ===========================================================================
123
124use crate::breakdown::Breakdown;
125use crate::dynamic::Scorable;
126use crate::float::Float;
127use crate::value::{GtZero, NormalizedContainer, NormalizedWeight, Value01};
128use core::marker::PhantomData;
129use witnessed::{WitnessExt, Witnessed};
130
131// ---------------------------------------------------------------------------
132// FiniteMember — a single weighted metric in a FiniteScoreSet
133// ---------------------------------------------------------------------------
134
135/// A member of a [`FiniteScoreSet`]: a normalized weight paired with a metric
136/// enum variant.
137///
138/// See [`Member`](crate::Member) for the Layer-1 equivalent.
139pub struct FiniteMember<T: Float, E> {
140    /// The normalized weight.
141    pub weight: Witnessed<T, NormalizedWeight>,
142    /// The metric enum variant.
143    pub metric: E,
144}
145
146impl<T: Float, E> FiniteMember<T, E> {
147    /// Compute the weighted contribution of a metric score.
148    ///
149    /// `contribute(score) = score × normalized_weight`
150    #[inline]
151    pub fn contribute(&self, value: Witnessed<T, Value01>) -> T {
152        value.into_inner() * self.weight.into_inner()
153    }
154
155    /// Return a reference to the metric.
156    #[inline]
157    pub fn metric(&self) -> &E {
158        &self.metric
159    }
160}
161
162// ---------------------------------------------------------------------------
163// FiniteScoreSet — weighted set with enum-based static dispatch (Layer 2)
164// ---------------------------------------------------------------------------
165
166/// A weighted set of scoring operators using enum-based static dispatch.
167///
168/// `FiniteScoreSet` stores a `Vec` of [`FiniteMember`]s, each wrapping a
169/// variant of a user-declared metric enum. At evaluation time, the enum's
170/// `eval` method dispatches via `match` — zero vtable overhead for all
171/// non-`Custom` variants.
172///
173/// Construct via [`finite_score_set!`](crate::finite_score_set!), the
174/// [`FiniteScoreSetBuilder`], or call [`.score()`](FiniteScoreSet::score)
175/// directly.
176///
177/// # Type parameters
178///
179/// - `T: Float` — the floating-point type (`f32` or `f64`).
180/// - `I` — the input type passed to each metric.
181/// - `E: Scorable<T, I>` — the metric enum generated by
182///   [`finite_metric!`](crate::finite_metric!).
183///
184/// # Example
185///
186/// ```ignore
187/// let set = FiniteScoreSet::<f64, &str, TestKind<f64, &str>>::normalize(vec![
188///     (2.0, TestKind::AlwaysZero(ConstMetric::new("zero", 0.0))),
189///     (3.0, TestKind::AlwaysOne(ConstMetric::new("one", 1.0))),
190/// ])?;
191///
192/// let total = set.sum(&"input");
193/// // total = 0.4 * 0 + 0.6 * 1 = 0.6
194/// ```
195pub struct FiniteScoreSet<T: Float, I, E> {
196    members: Vec<FiniteMember<T, E>>,
197    _phantom: PhantomData<I>,
198}
199
200impl<T: Float, I, E: Scorable<T, I>> FiniteScoreSet<T, I, E> {
201    /// Normalize raw weights and validate the resulting set.
202    ///
203    /// Each weight must be finite and strictly positive. Weights are normalized
204    /// to sum to 1.
205    #[doc(hidden)]
206    pub fn normalize(entries: Vec<(T, E)>) -> Result<Self, &'static str> {
207        if entries.is_empty() {
208            return Err("FiniteScoreSet: must have at least one member");
209        }
210
211        // Validate all weights are > 0
212        for (w, _) in &entries {
213            GtZero::witness(*w)?;
214        }
215
216        let sum: T = entries.iter().fold(T::zero(), |acc, (w, _)| acc + *w);
217
218        let mut normalized: Vec<T> = entries.iter().map(|(w, _)| *w / sum).collect();
219
220        // Sort a copy for binary search in NormalizedWeight
221        let mut sorted = normalized.clone();
222        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
223        let container = NormalizedContainer::witness(sorted)?;
224
225        let members: Vec<FiniteMember<T, E>> = entries
226            .into_iter()
227            .zip(normalized.drain(..))
228            .map(|((_, metric), nw)| {
229                let weight = nw
230                    .witness()
231                    .by(|v| NormalizedWeight::from_normalized_container(*v, &container))?;
232                Ok(FiniteMember { weight, metric })
233            })
234            .collect::<Result<Vec<_>, &'static str>>()?;
235
236        Ok(FiniteScoreSet {
237            members,
238            _phantom: PhantomData,
239        })
240    }
241
242    /// Evaluate all metrics against `input` and sum their weighted contributions.
243    ///
244    /// This is the most common aggregation: each metric is evaluated, multiplied
245    /// by its normalized weight, and summed. Zero-allocation convenience.
246    ///
247    /// For custom aggregation, use [`.score()`](Self::score) instead.
248    #[inline]
249    pub fn sum(&self, input: &I) -> T {
250        self.members
251            .iter()
252            .fold(T::zero(), |acc, m| acc + m.contribute(m.metric.eval(input)))
253    }
254
255    /// Enter the scoring stage, returning a reference to all members.
256    ///
257    /// Use [`.by()`](FiniteScoreStage::by) on the returned stage to apply a
258    /// custom aggregation, or [`.sum()`](Self::sum) for the standard
259    /// weighted-sum shortcut.
260    ///
261    /// # Example
262    ///
263    /// ```ignore
264    /// let total = set.score().by(|members| {
265    ///     members.iter().fold(0.0, |acc, m| {
266    ///         acc + m.contribute(m.metric().eval(&input))
267    ///     })
268    /// });
269    /// ```
270    #[inline]
271    pub fn score(&self) -> FiniteScoreStage<'_, T, I, E> {
272        FiniteScoreStage {
273            members: &self.members,
274            _phantom: PhantomData,
275        }
276    }
277
278    /// Return the number of members in this set.
279    #[inline]
280    pub fn len(&self) -> usize {
281        self.members.len()
282    }
283
284    /// Return `true` if the set has no members.
285    #[inline]
286    pub fn is_empty(&self) -> bool {
287        self.members.is_empty()
288    }
289
290    /// Iterate over the members.
291    #[inline]
292    pub fn iter(&self) -> impl Iterator<Item = &FiniteMember<T, E>> {
293        self.members.iter()
294    }
295
296    /// Evaluate all metrics against `input` and return a per-metric breakdown.
297    ///
298    /// Unlike [`.sum()`](Self::sum) which returns only the aggregate,
299    /// `breakdown` returns one [`Breakdown`] row per member with the metric's
300    /// name, raw score, normalized weight, and weighted contribution.
301    #[inline]
302    pub fn breakdown(&self, input: &I) -> Vec<Breakdown<'_, T>> {
303        self.members
304            .iter()
305            .map(|m| {
306                let score_witness = m.metric.eval(input);
307                let score_val: T = *score_witness;
308                Breakdown {
309                    name: m.metric.name(),
310                    score: score_val,
311                    weight: m.weight.into_inner(),
312                    contribution: m.contribute(score_witness),
313                }
314            })
315            .collect()
316    }
317
318    /// Create a builder for incremental construction of a `FiniteScoreSet`.
319    ///
320    /// Use this when members are not known up front — push them one by one,
321    /// then call [`.build()`](FiniteScoreSetBuilder::build) to finalize.
322    #[inline]
323    pub fn builder() -> FiniteScoreSetBuilder<T, I, E> {
324        FiniteScoreSetBuilder {
325            entries: Vec::new(),
326            _phantom: PhantomData,
327        }
328    }
329}
330
331// ---------------------------------------------------------------------------
332// FiniteScoreStage — member reference for custom aggregation (Layer 2)
333// ---------------------------------------------------------------------------
334
335/// The scoring stage for a [`FiniteScoreSet`], created by
336/// [`FiniteScoreSet::score`].
337///
338/// Holds a reference to the set's members. Call
339/// [`.by()`](FiniteScoreStage::by) to apply a custom aggregation over the
340/// member slice. For the standard weighted-sum shortcut, use
341/// [`FiniteScoreSet::sum`] instead.
342///
343/// # Examples
344///
345/// ```ignore
346/// // Standard weighted sum via the stage:
347/// let total = set.score().by(|members| {
348///     members.iter().fold(0.0, |acc, m| {
349///         acc + m.contribute(m.metric().eval(&input))
350///     })
351/// });
352///
353/// // Custom: use only the worst contribution
354/// let worst = set.score().by(|members| {
355///     members.iter().map(|m| {
356///         m.contribute(m.metric().eval(&input))
357///     }).fold(f64::INFINITY, f64::min)
358/// });
359/// ```
360pub struct FiniteScoreStage<'a, T: Float, I, E> {
361    members: &'a [FiniteMember<T, E>],
362    _phantom: PhantomData<I>,
363}
364
365impl<'a, T: Float, I, E: Scorable<T, I>> FiniteScoreStage<'a, T, I, E> {
366    /// Apply a custom aggregation to the members.
367    ///
368    /// The closure receives a `&[FiniteMember<T, E>]` — one entry per member
369    /// in insertion order. Each [`FiniteMember`] provides
370    /// [`.metric()`](FiniteMember::metric) for evaluation and
371    /// [`.contribute()`](FiniteMember::contribute) for weighting. The closure
372    /// may return any type `R`.
373    #[inline]
374    pub fn by<F, R>(self, f: F) -> R
375    where
376        F: FnOnce(&[FiniteMember<T, E>]) -> R,
377    {
378        f(self.members)
379    }
380}
381
382// ---------------------------------------------------------------------------
383// FiniteScoreSetBuilder — incremental builder for FiniteScoreSet
384// ---------------------------------------------------------------------------
385
386/// Incremental builder for [`FiniteScoreSet`].
387///
388/// Accumulates raw `(weight, variant)` pairs via [`.push()`](Self::push), then
389/// normalizes them into a [`FiniteScoreSet`] via [`.build()`](Self::build).
390///
391/// Each weight is validated on push (must be finite and > 0). Normalization
392/// happens once at build time.
393///
394/// # Examples
395///
396/// Chain construction:
397///
398/// ```ignore
399/// let set = FiniteScoreSet::<f64, &str, TestKind<f64, &str>>::builder()
400///     .push(2.0, TestKind::AlwaysZero(const_metric("zero", 0.0)))?
401///     .push(3.0, TestKind::AlwaysOne(const_metric("one", 1.0)))?
402///     .build()?;
403/// ```
404///
405/// Conditional construction:
406///
407/// ```ignore
408/// let mut builder = FiniteScoreSet::<f64, &str, TestKind<f64, &str>>::builder();
409/// builder = builder.push(2.0, baseline_variant)?;
410/// if enable_extra {
411///     builder = builder.push(1.0, extra_variant)?;
412/// }
413/// let set = builder.build()?;
414/// ```
415pub struct FiniteScoreSetBuilder<T: Float, I, E> {
416    entries: Vec<(T, E)>,
417    _phantom: PhantomData<I>,
418}
419
420impl<T: Float, I, E: Scorable<T, I>> FiniteScoreSetBuilder<T, I, E> {
421    /// Push a metric enum variant with a raw weight into the builder.
422    ///
423    /// The weight must be finite and strictly positive. This is validated
424    /// immediately (fail-fast). Takes and returns `Self` for chaining.
425    ///
426    /// For incremental construction, rebind the result:
427    ///
428    /// ```ignore
429    /// let mut builder = FiniteScoreSet::builder();
430    /// builder = builder.push(2.0, variant_a)?;
431    /// if some_condition {
432    ///     builder = builder.push(1.0, variant_b)?;
433    /// }
434    /// let set = builder.build()?;
435    /// ```
436    ///
437    /// # Errors
438    ///
439    /// Returns an error if `weight` is zero, negative, or not finite.
440    #[inline]
441    pub fn push(mut self, weight: T, variant: E) -> Result<Self, &'static str> {
442        GtZero::witness(weight)?;
443        self.entries.push((weight, variant));
444        Ok(self)
445    }
446
447    /// Consume the builder and produce a [`FiniteScoreSet`] with normalized
448    /// weights.
449    ///
450    /// # Errors
451    ///
452    /// Returns an error if no members were pushed.
453    #[inline]
454    pub fn build(self) -> Result<FiniteScoreSet<T, I, E>, &'static str> {
455        FiniteScoreSet::normalize(self.entries)
456    }
457}
458
459#[cfg(test)]
460mod tests_for_finite;