Skip to main content

khive_fold/objective/
selection.rs

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