@@ -4,6 +4,15 @@ use std::collections::HashSet;
44use crate :: geo:: { haversine_batch, haversine_distance, EARTH_RADIUS_KM } ;
55use crate :: model:: { Airport , Disc , OutputRecord } ;
66
7+ /// Result of geolocating a single MIS cluster.
8+ pub struct GeolocationResult {
9+ pub airport : Airport ,
10+ /// Max pairwise distance (km) between surviving candidate cities
11+ pub candidate_diameter : f32 ,
12+ /// Number of discs that successfully narrowed the candidate set
13+ pub num_constraints : u32 ,
14+ }
15+
716pub struct AnycastAnalyzer < ' a > {
817 /// Alpha parameter (distance/population tuner)
918 alpha : f32 ,
@@ -15,6 +24,8 @@ pub struct AnycastAnalyzer<'a> {
1524 all_discs : Vec < Disc > ,
1625 /// Boolean, returns only geolocation for anycast targets if true
1726 anycast_only : bool ,
27+ /// Whether to compute accuracy metrics
28+ accuracy : bool ,
1829}
1930
2031impl < ' a > AnycastAnalyzer < ' a > {
@@ -26,6 +37,7 @@ impl<'a> AnycastAnalyzer<'a> {
2637 alpha : f32 ,
2738 pop_ratio : f32 ,
2839 anycast_only : bool ,
40+ accuracy : bool ,
2941 ) -> Self {
3042 // Sort discs from lowest to highest
3143 discs. sort_unstable_by ( |a, b| a. radius . partial_cmp ( & b. radius ) . unwrap ( ) ) ;
@@ -35,6 +47,7 @@ impl<'a> AnycastAnalyzer<'a> {
3547 airport_tree,
3648 all_discs : discs,
3749 anycast_only,
50+ accuracy,
3851 }
3952 }
4053
@@ -67,23 +80,25 @@ impl<'a> AnycastAnalyzer<'a> {
6780 let geolocation_result = self . geolocation ( & cluster) ;
6881
6982 // Return the geolocation result (in expected output format)
70- if let Some ( best_airport ) = geolocation_result {
71- if chosen_airports. contains ( & best_airport . iata ) {
83+ if let Some ( geo ) = geolocation_result {
84+ if chosen_airports. contains ( & geo . airport . iata ) {
7285 continue ;
7386 }
74- chosen_airports. insert ( best_airport . iata . clone ( ) ) ;
87+ chosen_airports. insert ( geo . airport . iata . clone ( ) ) ;
7588
7689 results. push ( OutputRecord {
7790 target : target_ip. clone ( ) ,
7891 vp : disc_in_mis. hostname . clone ( ) ,
7992 vp_lat : disc_in_mis. lat . to_degrees ( ) ,
8093 vp_lon : disc_in_mis. lon . to_degrees ( ) ,
8194 radius : disc_in_mis. radius ,
82- pop_iata : best_airport. iata . clone ( ) ,
83- pop_lat : best_airport. lat ,
84- pop_lon : best_airport. lon ,
85- pop_city : best_airport. city . clone ( ) ,
86- pop_cc : best_airport. country_code . clone ( ) ,
95+ pop_iata : geo. airport . iata . clone ( ) ,
96+ pop_lat : geo. airport . lat ,
97+ pop_lon : geo. airport . lon ,
98+ pop_city : geo. airport . city . clone ( ) ,
99+ pop_cc : geo. airport . country_code . clone ( ) ,
100+ candidate_diameter : self . accuracy . then_some ( geo. candidate_diameter ) ,
101+ num_constraints : self . accuracy . then_some ( geo. num_constraints ) ,
87102 } ) ;
88103 } else {
89104 results. push ( OutputRecord {
@@ -97,6 +112,8 @@ impl<'a> AnycastAnalyzer<'a> {
97112 pop_lon : disc_in_mis. lon . to_degrees ( ) ,
98113 pop_city : "N/A" . to_string ( ) ,
99114 pop_cc : "N/A" . to_string ( ) ,
115+ candidate_diameter : self . accuracy . then_some ( 0.0 ) ,
116+ num_constraints : self . accuracy . then_some ( 0 ) ,
100117 } ) ;
101118 }
102119 }
@@ -205,7 +222,7 @@ impl<'a> AnycastAnalyzer<'a> {
205222 }
206223
207224 /// Geolocate the best location for an MIS cluster of discs
208- fn geolocation ( & self , cluster : & [ & Disc ] ) -> Option < Airport > {
225+ fn geolocation ( & self , cluster : & [ & Disc ] ) -> Option < GeolocationResult > {
209226 // Get the MIS for this cluster (smallest circle)
210227 let smallest = cluster
211228 . iter ( )
@@ -257,15 +274,19 @@ impl<'a> AnycastAnalyzer<'a> {
257274 let mut batch_dists = vec ! [ 0.0f32 ; n_apts] ;
258275
259276 // Progressively intersect (reducing bbox size)
277+ // Start at 1: the bbox pre-filter already applies the smallest disc's constraint
278+ let mut num_constraints: u32 = 1 ;
260279 for disc in & sorted_cluster {
261280 prev_alive. copy_from_slice ( & alive) ;
262281 // Calculate distance between current discs and all eligible locations
263282 haversine_batch ( disc. lat , disc. lon , & apt_lats, & apt_lons, & mut batch_dists) ;
264283
265284 // Eliminate cities outside of this disc
285+ let mut changed = false ;
266286 for i in 0 ..n_apts {
267287 if alive[ i] && batch_dists[ i] > disc. radius {
268288 alive[ i] = false ;
289+ changed = true ;
269290 }
270291 }
271292
@@ -274,6 +295,11 @@ impl<'a> AnycastAnalyzer<'a> {
274295 alive. copy_from_slice ( & prev_alive) ;
275296 break ;
276297 }
298+
299+ // Count discs that actually narrowed the candidate set
300+ if changed {
301+ num_constraints += 1 ;
302+ }
277303 }
278304
279305 // Get all eligible locations that survived
@@ -302,6 +328,27 @@ impl<'a> AnycastAnalyzer<'a> {
302328 let total_pop: f32 = candidates. iter ( ) . map ( |( a, _) | a. pop as f32 ) . sum ( ) ;
303329 let total_dist: f32 = candidates. iter ( ) . map ( |( _, d) | * d) . sum ( ) ;
304330
331+ // Compute candidate diameter: max pairwise distance between surviving candidates
332+ let candidate_diameter = if self . accuracy && candidates. len ( ) > 1 {
333+ let mut max_dist: f32 = 0.0 ;
334+ for i in 0 ..candidates. len ( ) {
335+ for j in ( i + 1 ) ..candidates. len ( ) {
336+ let d = haversine_distance (
337+ candidates[ i] . 0 . lat_rad ,
338+ candidates[ i] . 0 . lon_rad ,
339+ candidates[ j] . 0 . lat_rad ,
340+ candidates[ j] . 0 . lon_rad ,
341+ ) ;
342+ if d > max_dist {
343+ max_dist = d;
344+ }
345+ }
346+ }
347+ max_dist
348+ } else {
349+ 0.0
350+ } ;
351+
305352 // Return the city with the highest score
306353 candidates
307354 . into_iter ( )
@@ -316,6 +363,10 @@ impl<'a> AnycastAnalyzer<'a> {
316363
317364 score1. partial_cmp ( & score2) . unwrap_or ( std:: cmp:: Ordering :: Equal )
318365 } )
319- . map ( |( a, _) | a. clone ( ) )
366+ . map ( |( a, _) | GeolocationResult {
367+ airport : a. clone ( ) ,
368+ candidate_diameter,
369+ num_constraints,
370+ } )
320371 }
321372}
0 commit comments