Skip to content

Commit 6b58bb3

Browse files
committed
POC improve transfers
1 parent 6f90504 commit 6b58bb3

File tree

4 files changed

+225
-14
lines changed

4 files changed

+225
-14
lines changed

gtfs2ntfs/src/main.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,13 +137,14 @@ fn run(opt: Opt) -> Result<()> {
137137
let model = if opt.ignore_transfers {
138138
model
139139
} else {
140-
generates_transfers(
140+
let collections = generates_transfers(
141141
model,
142142
opt.max_distance,
143143
opt.walking_speed,
144144
opt.waiting_time,
145145
None,
146-
)?
146+
)?;
147+
transit_model::Model::new(collections)?
147148
};
148149

149150
match opt.output.extension() {

ntfs2ntfs/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ fn run(opt: Opt) -> Result<()> {
9494

9595
let model = transit_model::ntfs::read(opt.input)?;
9696
let model = if opt.ignore_transfers {
97-
model
97+
model.into_collections()
9898
} else {
9999
generates_transfers(
100100
model,

src/ntfs/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ where
352352
/// [NTFS](https://github.com/hove-io/ntfs-specification/blob/master/ntfs_fr.md)
353353
/// files in the given directory.
354354
pub fn write<P: AsRef<path::Path>>(
355-
model: &Model,
355+
model: &Collections,
356356
path: P,
357357
current_datetime: DateTime<FixedOffset>,
358358
) -> Result<()> {
@@ -437,7 +437,7 @@ pub fn write<P: AsRef<path::Path>>(
437437
/// [NTFS](https://github.com/hove-io/ntfs-specification/blob/master/ntfs_fr.md)
438438
/// ZIP archive at the given full path.
439439
pub fn write_to_zip<P: AsRef<path::Path>>(
440-
model: &Model,
440+
model: &Collections,
441441
path: P,
442442
current_datetime: DateTime<FixedOffset>,
443443
) -> Result<()> {

src/transfers.rs

Lines changed: 219 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@
1515
//! See function generates_transfers
1616
1717
use crate::{
18-
model::Model,
18+
model::{Collections, Model},
1919
objects::{Coord, StopPoint, Transfer},
2020
Result,
2121
};
2222
use std::collections::HashMap;
23+
use std::time::Instant;
2324
use tracing::info;
2425
use 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²)
53119
pub 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

Comments
 (0)