1515//! See function generates_transfers
1616
1717use crate :: {
18- model:: Model ,
18+ model:: { Collections , Model } ,
1919 objects:: { Coord , StopPoint , Transfer } ,
2020 Result ,
2121} ;
2222use std:: collections:: HashMap ;
23+ use std:: time:: Instant ;
2324use tracing:: info;
2425use typed_index_collection:: { Collection , CollectionWithId , Idx } ;
2526
@@ -49,7 +50,72 @@ pub fn get_available_transfers(
4950 . collect ( )
5051}
5152
53+ /// Spatial grid to efficiently find nearby stop points
54+ struct SpatialGrid {
55+ /// Cell size in degrees (approximation)
56+ cell_size : f64 ,
57+ /// Map from (cell_x, cell_y) to list of stop_point_idx
58+ cells : HashMap < ( i32 , i32 ) , Vec < Idx < StopPoint > > > ,
59+ }
60+
61+ impl SpatialGrid {
62+ /// Create a new spatial grid with cells sized to contain points within max_distance
63+ /// We make cells larger (3x max_distance) to reduce the number of cells to check
64+ fn new ( max_distance : f64 ) -> Self {
65+ // Approximate cell size in degrees (at equator, 1 degree ≈ 111km)
66+ // Use 3x max_distance so we only need to check current cell + immediate neighbors
67+ let cell_size = ( max_distance * 3.0 ) / 111_000.0 ; // Convert meters to degrees
68+ Self {
69+ cell_size,
70+ cells : HashMap :: new ( ) ,
71+ }
72+ }
73+
74+ /// Get the cell coordinates for a given coordinate
75+ #[ inline]
76+ fn get_cell ( & self , coord : & Coord ) -> ( i32 , i32 ) {
77+ (
78+ ( coord. lon / self . cell_size ) . floor ( ) as i32 ,
79+ ( coord. lat / self . cell_size ) . floor ( ) as i32 ,
80+ )
81+ }
82+
83+ /// Insert a stop point into the grid
84+ fn insert ( & mut self , idx : Idx < StopPoint > , coord : & Coord ) {
85+ let cell = self . get_cell ( coord) ;
86+ self . cells . entry ( cell) . or_default ( ) . push ( idx) ;
87+ }
88+
89+ /// Fill the provided vector with stop point indices in the cell and adjacent cells (3x3 grid)
90+ /// This reuses the Vec buffer to avoid allocations
91+ #[ inline]
92+ fn get_nearby_indices_into ( & self , coord : & Coord , result : & mut Vec < Idx < StopPoint > > ) {
93+ result. clear ( ) ;
94+ let ( cell_x, cell_y) = self . get_cell ( coord) ;
95+
96+ // Check the 9 cells: current + 8 adjacent
97+ // Use saturating_add to avoid overflow with extreme coordinates
98+ for dx in -1 ..=1 {
99+ for dy in -1 ..=1 {
100+ let target_x = cell_x. saturating_add ( dx) ;
101+ let target_y = cell_y. saturating_add ( dy) ;
102+ if let Some ( indices) = self . cells . get ( & ( target_x, target_y) ) {
103+ result. extend_from_slice ( indices) ;
104+ }
105+ }
106+ }
107+ }
108+ }
109+
52110/// Generate missing transfers from stop points within the required distance
111+ ///
112+ /// This function uses a spatial grid optimization to avoid O(n²) complexity.
113+ /// Instead of comparing every stop point with every other stop point, it:
114+ /// 1. Divides the geographic space into a grid of cells
115+ /// 2. For each stop point, only checks points in the same cell and adjacent cells (3x3 grid)
116+ ///
117+ /// Complexity: O(n × k) where k is the average number of points per cell neighborhood
118+ /// For uniformly distributed points, this is approximately O(n) instead of O(n²)
53119pub fn generate_missing_transfers_from_sp (
54120 transfers_map : & TransferMap ,
55121 model : & Model ,
@@ -58,18 +124,51 @@ pub fn generate_missing_transfers_from_sp(
58124 waiting_time : u32 ,
59125 need_transfer : Option < NeedTransfer > ,
60126) -> TransferMap {
127+ let total_start = Instant :: now ( ) ;
61128 info ! ( "Adding missing transfers from stop points." ) ;
62129 let mut new_transfers_map = TransferMap :: new ( ) ;
63130 let sq_max_distance = max_distance * max_distance;
131+
132+ // Build spatial grid for efficient proximity queries
133+ let grid_start = Instant :: now ( ) ;
134+ let mut grid = SpatialGrid :: new ( max_distance) ;
135+ let mut valid_stop_count = 0 ;
136+ for ( idx, sp) in model. stop_points . iter ( ) {
137+ if sp. coord != Coord :: default ( ) {
138+ grid. insert ( idx, & sp. coord ) ;
139+ valid_stop_count += 1 ;
140+ }
141+ }
142+ let grid_duration = grid_start. elapsed ( ) ;
143+ info ! (
144+ "Built spatial grid with {} cells for {} valid stop points in {:.2?}" ,
145+ grid. cells. len( ) ,
146+ valid_stop_count,
147+ grid_duration
148+ ) ;
149+
150+ // Pre-allocate buffer for nearby indices to avoid repeated allocations
151+ let mut nearby_indices = Vec :: with_capacity ( 100 ) ;
152+
153+ let compute_start = Instant :: now ( ) ;
154+ let mut total_comparisons = 0_u64 ;
155+ let mut total_transfers_created = 0_u64 ;
156+ let mut total_distance_checks = 0_u64 ;
157+
158+ // For each stop point, only check nearby points from the grid
64159 for ( idx1, sp1) in model. stop_points . iter ( ) {
65160 if sp1. coord == Coord :: default ( ) {
66161 continue ;
67162 }
68163 let approx = sp1. coord . approx ( ) ;
69- for ( idx2, sp2) in model. stop_points . iter ( ) {
70- if sp2. coord == Coord :: default ( ) {
71- continue ;
72- }
164+
165+ // Get nearby point indices (same cell + adjacent cells) - reuses the buffer
166+ grid. get_nearby_indices_into ( & sp1. coord , & mut nearby_indices) ;
167+
168+ // Only iterate over nearby points
169+ for & idx2 in & nearby_indices {
170+ total_comparisons += 1 ;
171+
73172 if transfers_map. contains_key ( & ( idx1, idx2) ) {
74173 continue ;
75174 }
@@ -78,10 +177,15 @@ pub fn generate_missing_transfers_from_sp(
78177 continue ;
79178 }
80179 }
180+ let sp2 = & model. stop_points [ idx2] ;
181+
182+ total_distance_checks += 1 ;
81183 let sq_distance = approx. sq_distance_to ( & sp2. coord ) ;
82184 if sq_distance > sq_max_distance {
83185 continue ;
84186 }
187+
188+ total_transfers_created += 1 ;
85189 let transfer_time = ( sq_distance. sqrt ( ) / walking_speed) as u32 ;
86190 new_transfers_map. insert (
87191 ( idx1, idx2) ,
@@ -95,6 +199,22 @@ pub fn generate_missing_transfers_from_sp(
95199 ) ;
96200 }
97201 }
202+
203+ let compute_duration = compute_start. elapsed ( ) ;
204+ let total_duration = total_start. elapsed ( ) ;
205+
206+ info ! (
207+ "Transfer computation stats: {} comparisons, {} distance checks, {} transfers created in {:.2?}" ,
208+ total_comparisons,
209+ total_distance_checks,
210+ total_transfers_created,
211+ compute_duration
212+ ) ;
213+ info ! (
214+ "Total time for generate_missing_transfers_from_sp: {:.2?}" ,
215+ total_duration
216+ ) ;
217+
98218 new_transfers_map
99219}
100220
@@ -130,9 +250,19 @@ pub fn generates_transfers(
130250 walking_speed : f64 ,
131251 waiting_time : u32 ,
132252 need_transfer : Option < NeedTransfer > ,
133- ) -> Result < Model > {
253+ ) -> Result < Collections > {
254+ let total_start = Instant :: now ( ) ;
134255 info ! ( "Generating transfers..." ) ;
256+
257+ let get_transfers_start = Instant :: now ( ) ;
135258 let mut transfers_map = get_available_transfers ( model. transfers . clone ( ) , & model. stop_points ) ;
259+ info ! (
260+ "get_available_transfers: {} existing transfers in {:.2?}" ,
261+ transfers_map. len( ) ,
262+ get_transfers_start. elapsed( )
263+ ) ;
264+
265+ let gen_transfers_start = Instant :: now ( ) ;
136266 let new_transfers_map = generate_missing_transfers_from_sp (
137267 & transfers_map,
138268 & model,
@@ -141,16 +271,39 @@ pub fn generates_transfers(
141271 waiting_time,
142272 need_transfer,
143273 ) ;
274+ info ! (
275+ "generate_missing_transfers_from_sp returned {} new transfers in {:.2?}" ,
276+ new_transfers_map. len( ) ,
277+ gen_transfers_start. elapsed( )
278+ ) ;
144279
280+ let merge_start = Instant :: now ( ) ;
145281 transfers_map. extend ( new_transfers_map) ;
146282 let mut new_transfers: Vec < _ > = transfers_map. into_values ( ) . collect ( ) ;
283+ info ! ( "Merged transfers in {:.2?}" , merge_start. elapsed( ) ) ;
284+
285+ let sort_start = Instant :: now ( ) ;
147286 new_transfers. sort_unstable_by ( |t1, t2| {
148287 ( & t1. from_stop_id , & t1. to_stop_id ) . cmp ( & ( & t2. from_stop_id , & t2. to_stop_id ) )
149288 } ) ;
289+ info ! (
290+ "Sorted {} transfers in {:.2?}" ,
291+ new_transfers. len( ) ,
292+ sort_start. elapsed( )
293+ ) ;
150294
295+ let rebuild_start = Instant :: now ( ) ;
151296 let mut collections = model. into_collections ( ) ;
152297 collections. transfers = Collection :: new ( new_transfers) ;
153- Model :: new ( collections)
298+ // let result = Model::new(collections);
299+ info ! ( "Rebuilt model in {:.2?}" , rebuild_start. elapsed( ) ) ;
300+
301+ info ! (
302+ "generates_transfers TOTAL TIME: {:.2?}" ,
303+ total_start. elapsed( )
304+ ) ;
305+
306+ Ok ( collections)
154307}
155308
156309#[ cfg( test) ]
@@ -361,8 +514,8 @@ mod tests {
361514 #[ test]
362515 fn test_generates_transfers ( ) {
363516 let model = base_model ( ) ;
364- let new_model = generates_transfers ( model , 100.0 , 0.7 , 2 , None ) . expect ( "an error occured" ) ;
365- let mut collections = new_model . into_collections ( ) ;
517+ let mut collections =
518+ generates_transfers ( model , 100.0 , 0.7 , 2 , None ) . expect ( "an error occured" ) ;
366519
367520 let mut transfers = Collection :: new ( vec ! [
368521 Transfer {
@@ -441,4 +594,61 @@ mod tests {
441594
442595 assert_eq ! ( transfers, transfers_expected) ;
443596 }
597+
598+ #[ test]
599+ fn test_spatial_grid_performance ( ) {
600+ // Create a model with many stop points to demonstrate the performance improvement
601+ let mut model_builder = ModelBuilder :: default ( ) ;
602+
603+ // Create a grid of stop points (e.g., 100 x 100 = 10,000 points)
604+ // In a real scenario with 10k points:
605+ // - O(n²) = 100,000,000 comparisons
606+ // - O(n×k) with spatial grid ≈ 10,000 × 9 cells × ~10 points = ~900,000 comparisons
607+ // This is roughly 100x faster!
608+
609+ let grid_size = 10 ; // Use 10x10 = 100 points for the test (to keep it fast)
610+ for i in 0 ..grid_size {
611+ for j in 0 ..grid_size {
612+ let stop_id = format ! ( "SP_{i}_{j}" ) ;
613+ // Create stops in a 0.01° x 0.01° grid (roughly 1km x 1km)
614+
615+ model_builder = model_builder. vj ( & format ! ( "vj_{i}_{j}" ) , |vj_builder| {
616+ vj_builder
617+ . route ( & format ! ( "route_{i}_{j}" ) )
618+ . st ( & stop_id, "10:00:00" ) ;
619+ } ) ;
620+ }
621+ }
622+
623+ let transit_model = model_builder. build ( ) ;
624+ let mut collections = transit_model. into_collections ( ) ;
625+
626+ // Set coordinates for all stop points
627+ for i in 0 ..grid_size {
628+ for j in 0 ..grid_size {
629+ let stop_id = format ! ( "SP_{i}_{j}" ) ;
630+ collections. stop_points . get_mut ( & stop_id) . unwrap ( ) . coord = Coord {
631+ lon : 2.39 + ( i as f64 ) * 0.001 ,
632+ lat : 48.85 + ( j as f64 ) * 0.001 ,
633+ } ;
634+ }
635+ }
636+
637+ let model = Model :: new ( collections) . unwrap ( ) ;
638+
639+ // Generate transfers with a reasonable distance (500m)
640+ let result = generates_transfers ( model, 500.0 , 0.7 , 2 , None ) ;
641+ assert ! ( result. is_ok( ) ) ;
642+
643+ // With spatial grid, this should complete quickly even with 100+ points
644+ // The number of transfers should be reasonable (not n²)
645+ let new_model = result. unwrap ( ) ;
646+ let transfer_count = new_model. transfers . len ( ) ;
647+
648+ // Each point should have transfers to nearby points (not all points)
649+ // With 100 points in a 10x10 grid and 500m max distance,
650+ // each point should connect to roughly 4-9 neighbors
651+ assert ! ( transfer_count < grid_size * grid_size * grid_size * grid_size) ;
652+ assert ! ( transfer_count > 0 ) ;
653+ }
444654}
0 commit comments