rusty_time_core/
select.rs1#[derive(Clone, Copy, Debug, PartialEq)]
6pub struct SourceEstimate {
7 pub id: usize,
9 pub offset: f64,
11 pub root_distance: f64,
13 pub stratum: u8,
14}
15
16#[derive(Clone, Debug, Default, PartialEq)]
17pub struct Selection {
18 pub truechimers: Vec<usize>,
20 pub offset: Option<f64>,
22}
23
24pub fn select(sources: &[SourceEstimate]) -> Selection {
28 let n = sources.len();
29 if n == 0 {
30 return Selection::default();
31 }
32
33 #[derive(Clone, Copy)]
36 struct Edge {
37 value: f64,
38 kind: i32, }
40 let mut edges: Vec<Edge> = Vec::with_capacity(2 * n);
41 for s in sources {
42 let rd = s.root_distance.max(1e-9);
43 edges.push(Edge {
44 value: s.offset - rd,
45 kind: 1,
46 });
47 edges.push(Edge {
48 value: s.offset + rd,
49 kind: -1,
50 });
51 }
52 edges.sort_by(|a, b| a.value.total_cmp(&b.value));
53
54 let mut chosen: Option<(f64, f64)> = None;
55 for f in 0..=(n.saturating_sub(1)) / 2 {
56 let need = (n - f) as i32;
57 let mut count = 0;
59 let mut low = None;
60 for e in &edges {
61 count += e.kind;
62 if count >= need {
63 low = Some(e.value);
64 break;
65 }
66 }
67 let mut count = 0;
69 let mut high = None;
70 for e in edges.iter().rev() {
71 count -= e.kind;
72 if count >= need {
73 high = Some(e.value);
74 break;
75 }
76 }
77 if let (Some(lo), Some(hi)) = (low, high)
78 && lo <= hi
79 {
80 chosen = Some((lo, hi));
81 break;
82 }
83 }
84
85 let Some((lo, hi)) = chosen else {
86 return Selection::default();
87 };
88
89 let mut survivors: Vec<&SourceEstimate> = sources
90 .iter()
91 .filter(|s| {
92 let rd = s.root_distance.max(1e-9);
93 s.offset + rd >= lo && s.offset - rd <= hi
94 })
95 .collect();
96 if survivors.is_empty() {
97 return Selection::default();
98 }
99
100 survivors.sort_by(|a, b| {
102 (a.stratum, a.root_distance)
103 .partial_cmp(&(b.stratum, b.root_distance))
104 .unwrap_or(core::cmp::Ordering::Equal)
105 });
106
107 let mut wsum = 0.0;
108 let mut osum = 0.0;
109 for s in &survivors {
110 let w = 1.0 / s.root_distance.max(1e-9);
111 wsum += w;
112 osum += w * s.offset;
113 }
114
115 Selection {
116 truechimers: survivors.iter().map(|s| s.id).collect(),
117 offset: (wsum > 0.0).then(|| osum / wsum),
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 fn src(id: usize, offset: f64, rd: f64) -> SourceEstimate {
126 SourceEstimate {
127 id,
128 offset,
129 root_distance: rd,
130 stratum: 2,
131 }
132 }
133
134 #[test]
135 fn falseticker_is_excluded() {
136 let sources = [
137 src(0, 0.001, 0.005),
138 src(1, 0.002, 0.005),
139 src(2, 0.0015, 0.005),
140 src(3, 0.500, 0.005), ];
142 let sel = select(&sources);
143 assert_eq!(sel.truechimers.len(), 3);
144 assert!(!sel.truechimers.contains(&3));
145 let o = sel.offset.expect("offset");
146 assert!(o > 0.0005 && o < 0.0035, "combined {o}");
147 }
148
149 #[test]
150 fn all_disjoint_yields_majority_failure() {
151 let sources = [
152 src(0, 0.0, 0.001),
153 src(1, 1.0, 0.001),
154 src(2, 2.0, 0.001),
155 src(3, 3.0, 0.001),
156 ];
157 let sel = select(&sources);
158 assert!(sel.offset.is_none() || sel.truechimers.len() <= 2);
160 }
161
162 #[test]
163 fn single_source_is_used() {
164 let sel = select(&[src(0, 0.010, 0.002)]);
165 assert_eq!(sel.truechimers, vec![0]);
166 assert!((sel.offset.expect("offset") - 0.010).abs() < 1e-12);
167 }
168
169 #[test]
170 fn empty_input_is_empty_output() {
171 assert_eq!(select(&[]), Selection::default());
172 }
173}