1use crate::marzullo;
2use crate::source::{SourceId, SourceMeasurement};
3use crate::timestamp::NtpDuration;
4
5#[derive(Clone, Debug)]
7pub struct SelectionResult {
8 pub system_offset: NtpDuration,
10 pub truechimers: Vec<SourceId>,
12 pub falsetickers: Vec<SourceId>,
14 pub system_peer: Option<SourceId>,
16 pub system_jitter: f64,
18}
19
20pub fn select_sources(measurements: &[SourceMeasurement]) -> SelectionResult {
27 if measurements.is_empty() {
28 return SelectionResult {
29 system_offset: NtpDuration::ZERO,
30 truechimers: Vec::new(),
31 falsetickers: Vec::new(),
32 system_peer: None,
33 system_jitter: 0.0,
34 };
35 }
36
37 let intersection = marzullo::intersect(measurements);
39
40 let truechimer_ids: Vec<SourceId> = intersection
41 .truechimers
42 .iter()
43 .map(|&i| measurements[i].id.clone())
44 .collect();
45
46 let falseticker_ids: Vec<SourceId> = intersection
47 .falsetickers
48 .iter()
49 .map(|&i| measurements[i].id.clone())
50 .collect();
51
52 if intersection.truechimers.is_empty() {
53 return SelectionResult {
54 system_offset: NtpDuration::ZERO,
55 truechimers: truechimer_ids,
56 falsetickers: falseticker_ids,
57 system_peer: None,
58 system_jitter: 0.0,
59 };
60 }
61
62 let mut best_peer_idx = intersection.truechimers[0];
64 let mut best_root_dist = measurements[best_peer_idx].root_distance();
65
66 for &idx in &intersection.truechimers[1..] {
67 let rd = measurements[idx].root_distance();
68 if rd.raw() < best_root_dist.raw() {
69 best_root_dist = rd;
70 best_peer_idx = idx;
71 }
72 }
73
74 let system_peer = Some(measurements[best_peer_idx].id.clone());
75
76 let mut weight_sum = 0.0_f64;
78 let mut weighted_offset_sum = 0.0_f64;
79
80 for &idx in &intersection.truechimers {
81 let rd = measurements[idx].root_distance().to_seconds_f64();
82 let w = if rd > 1e-15 { 1.0 / rd } else { 1e15 };
83 weight_sum += w;
84 weighted_offset_sum += w * measurements[idx].offset.to_seconds_f64();
85 }
86
87 let avg_offset_secs = if weight_sum > 0.0 {
88 weighted_offset_sum / weight_sum
89 } else {
90 0.0
91 };
92 let system_offset = NtpDuration::from_seconds_f64(avg_offset_secs);
93
94 let n_tc = intersection.truechimers.len() as f64;
96 let mut sum_sq = 0.0_f64;
97
98 for &idx in &intersection.truechimers {
99 let residual = measurements[idx].offset.to_seconds_f64() - avg_offset_secs;
100 sum_sq += residual * residual;
101 }
102
103 let system_jitter = if n_tc > 1.0 {
104 (sum_sq / (n_tc - 1.0)).sqrt()
105 } else {
106 measurements[intersection.truechimers[0]].jitter
107 };
108
109 SelectionResult {
110 system_offset,
111 truechimers: truechimer_ids,
112 falsetickers: falseticker_ids,
113 system_peer,
114 system_jitter,
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::clock::LeapIndicator;
122 use crate::timestamp::NtpTimestamp;
123 use std::net::SocketAddr;
124
125 fn make_measurement(
126 idx: u16,
127 offset_ms: f64,
128 delay_ms: f64,
129 root_delay_ms: f64,
130 root_dispersion_ms: f64,
131 ) -> SourceMeasurement {
132 let addr: SocketAddr = format!("127.0.0.{}:{}", idx, 123).parse().unwrap();
133 SourceMeasurement {
134 id: SourceId::Ntp {
135 address: addr,
136 reference_id: idx as u32,
137 },
138 offset: NtpDuration::from_seconds_f64(offset_ms / 1000.0),
139 delay: NtpDuration::from_seconds_f64(delay_ms / 1000.0),
140 dispersion: NtpDuration::from_seconds_f64(1.0 / 1000.0),
141 jitter: 0.001,
142 stratum: 2,
143 leap_indicator: LeapIndicator::NoWarning,
144 root_delay: NtpDuration::from_seconds_f64(root_delay_ms / 1000.0),
145 root_dispersion: NtpDuration::from_seconds_f64(root_dispersion_ms / 1000.0),
146 time: NtpTimestamp::ZERO,
147 }
148 }
149
150 #[test]
151 fn empty_input() {
152 let result = select_sources(&[]);
153 assert!(result.truechimers.is_empty());
154 assert!(result.falsetickers.is_empty());
155 assert!(result.system_peer.is_none());
156 assert_eq!(result.system_jitter, 0.0);
157 }
158
159 #[test]
160 fn single_source() {
161 let m = vec![make_measurement(1, 10.0, 5.0, 10.0, 5.0)];
162 let result = select_sources(&m);
163
164 assert_eq!(result.truechimers.len(), 1);
165 assert!(result.falsetickers.is_empty());
166 assert!(result.system_peer.is_some());
167 assert!(
168 (result.system_offset.to_millis_f64() - 10.0).abs() < 0.5,
169 "expected ~10ms, got {}ms",
170 result.system_offset.to_millis_f64()
171 );
172 }
173
174 #[test]
175 fn three_agreeing_sources() {
176 let m = vec![
177 make_measurement(1, 10.0, 5.0, 10.0, 5.0),
178 make_measurement(2, 11.0, 5.0, 10.0, 5.0),
179 make_measurement(3, 9.0, 5.0, 10.0, 5.0),
180 ];
181 let result = select_sources(&m);
182
183 assert_eq!(result.truechimers.len(), 3);
184 assert!(result.falsetickers.is_empty());
185 assert!(result.system_peer.is_some());
186 assert!(
187 (result.system_offset.to_millis_f64() - 10.0).abs() < 1.0,
188 "expected ~10ms, got {}ms",
189 result.system_offset.to_millis_f64()
190 );
191 assert!(result.system_jitter >= 0.0);
192 }
193
194 #[test]
195 fn one_falseticker_excluded() {
196 let m = vec![
197 make_measurement(1, 10.0, 5.0, 10.0, 5.0),
198 make_measurement(2, 11.0, 5.0, 10.0, 5.0),
199 make_measurement(3, 500.0, 5.0, 10.0, 5.0),
200 ];
201 let result = select_sources(&m);
202
203 assert_eq!(result.truechimers.len(), 2);
204 assert_eq!(result.falsetickers.len(), 1);
205 assert!(
206 result.system_offset.to_millis_f64() < 20.0,
207 "offset should not include falseticker, got {}ms",
208 result.system_offset.to_millis_f64()
209 );
210 }
211
212 #[test]
213 fn system_peer_has_lowest_root_distance() {
214 let m = vec![
215 make_measurement(1, 10.0, 5.0, 20.0, 5.0),
216 make_measurement(2, 11.0, 5.0, 2.0, 1.0),
217 make_measurement(3, 9.0, 5.0, 20.0, 5.0),
218 ];
219 let result = select_sources(&m);
220
221 assert!(result.system_peer.is_some());
222 let peer = result.system_peer.unwrap();
223 match &peer {
224 SourceId::Ntp { reference_id, .. } => {
225 assert_eq!(*reference_id, 2, "source 2 should be system peer");
226 }
227 _ => panic!("expected NTP source"),
228 }
229 }
230
231 #[test]
232 fn weighted_average_favors_lower_root_distance() {
233 let m = vec![
234 make_measurement(1, 0.0, 5.0, 40.0, 10.0),
235 make_measurement(2, 20.0, 5.0, 4.0, 1.0),
236 make_measurement(3, 0.0, 5.0, 40.0, 10.0),
237 ];
238 let result = select_sources(&m);
239
240 assert!(
241 result.system_offset.to_millis_f64() > 5.0,
242 "offset should be pulled toward 20ms by low-root-distance source, got {}ms",
243 result.system_offset.to_millis_f64()
244 );
245 }
246
247 #[test]
248 fn jitter_zero_for_identical_offsets() {
249 let m = vec![
250 make_measurement(1, 10.0, 5.0, 10.0, 5.0),
251 make_measurement(2, 10.0, 5.0, 10.0, 5.0),
252 make_measurement(3, 10.0, 5.0, 10.0, 5.0),
253 ];
254 let result = select_sources(&m);
255
256 assert!(
257 result.system_jitter < 1e-9,
258 "expected near-zero jitter, got {}",
259 result.system_jitter
260 );
261 }
262
263 #[test]
264 fn all_disagree_no_system_peer() {
265 let m = vec![
266 make_measurement(1, 0.0, 1.0, 1.0, 1.0),
267 make_measurement(2, 1000.0, 1.0, 1.0, 1.0),
268 make_measurement(3, -1000.0, 1.0, 1.0, 1.0),
269 ];
270 let result = select_sources(&m);
271
272 if result.truechimers.is_empty() {
273 assert!(result.system_peer.is_none());
274 }
275 }
276}