Skip to main content

khive_fold/objective/
selection.rs

1//! Selection result from objective functions
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6/// A selection result from an objective function
7#[derive(Debug, Clone)]
8#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9#[must_use = "selections should be used after creation"]
10pub struct Selection<T> {
11    /// The selected item
12    pub item: T,
13    /// Score of the selection
14    pub score: f64,
15    /// Precision (inverse variance) of the score estimate. Default 1.0 (fully trusted).
16    ///
17    /// The effective ranking score is `score * precision`. When precision is 1.0 (the
18    /// default), ranking is identical to raw score ordering.
19    #[cfg_attr(feature = "serde", serde(default = "default_precision"))]
20    pub precision: f64,
21    /// Index in the original candidates
22    pub index: usize,
23    /// Number of candidates considered
24    pub considered: usize,
25    /// Number of candidates that passed threshold
26    pub passed: usize,
27    /// Reason for selection
28    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
29    pub reason: Option<String>,
30}
31
32fn default_precision() -> f64 {
33    1.0
34}
35
36impl<T> Selection<T> {
37    /// Create a new selection
38    pub fn new(item: T, score: f64, index: usize) -> Self {
39        Self {
40            item,
41            score,
42            precision: 1.0,
43            index,
44            considered: 1,
45            passed: 1,
46            reason: None,
47        }
48    }
49
50    /// Set the precision (reliability estimate for the score).
51    ///
52    /// Values in (0, 1] are typical; 1.0 means fully trusted (the default).
53    pub fn with_precision(mut self, precision: f64) -> Self {
54        self.precision = precision;
55        self
56    }
57
58    /// Set the considered count
59    pub fn with_considered(mut self, n: usize) -> Self {
60        self.considered = n;
61        self
62    }
63
64    /// Set the passed count
65    pub fn with_passed(mut self, n: usize) -> Self {
66        self.passed = n;
67        self
68    }
69
70    /// Set the reason
71    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
72        self.reason = Some(reason.into());
73        self
74    }
75
76    /// Map the selected value
77    pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Selection<U> {
78        Selection {
79            item: f(self.item),
80            score: self.score,
81            precision: self.precision,
82            index: self.index,
83            considered: self.considered,
84            passed: self.passed,
85            reason: self.reason,
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn precision_default_is_one() {
96        let sel = Selection::new(42i32, 0.8, 0);
97        assert_eq!(sel.precision, 1.0);
98    }
99
100    #[test]
101    fn with_precision_sets_field() {
102        let sel = Selection::new(42i32, 0.8, 0).with_precision(0.5);
103        assert_eq!(sel.precision, 0.5);
104    }
105
106    #[test]
107    fn map_propagates_precision() {
108        let sel = Selection::new(42i32, 0.8, 0).with_precision(0.75);
109        let mapped = sel.map(|v| v.to_string());
110        assert_eq!(mapped.precision, 0.75);
111        assert_eq!(mapped.item, "42");
112        assert_eq!(mapped.score, 0.8);
113    }
114
115    #[test]
116    fn map_preserves_all_stats() {
117        let sel = Selection::new(1i32, 0.5, 2)
118            .with_precision(0.6)
119            .with_considered(10)
120            .with_passed(7)
121            .with_reason("test");
122        let mapped = sel.map(|v| v * 2);
123        assert_eq!(mapped.item, 2);
124        assert_eq!(mapped.score, 0.5);
125        assert_eq!(mapped.precision, 0.6);
126        assert_eq!(mapped.index, 2);
127        assert_eq!(mapped.considered, 10);
128        assert_eq!(mapped.passed, 7);
129        assert_eq!(mapped.reason.as_deref(), Some("test"));
130    }
131}