Skip to content

Commit 66368fa

Browse files
committed
Opt 12- build_graph() with spatial index (34%-54%)
1 parent a4dcdf7 commit 66368fa

File tree

7 files changed

+187
-61
lines changed

7 files changed

+187
-61
lines changed

benches/bench_offset_multiple.rs

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,48 @@
1+
use offroad::prelude::*;
12
use std::time::Instant;
23
use togo::prelude::*;
3-
use offroad::prelude::*;
44

55
fn main() {
66
println!("Multi-Offset Benchmark (based on offset_multi example)");
77
println!("======================================================");
8-
8+
99
let mut cfg = OffsetCfg::default();
1010
cfg.svg_orig = false;
1111
let poly_orig = pline_01()[0].clone();
1212
let poly = polyline_translate(&poly_orig, point(250.0, 100.0));
13-
13+
1414
let start = Instant::now();
15-
16-
// Forward direction
17-
for i in 1..500 {
18-
offset_polyline_to_polyline(&poly, (i as f64)/5.0, &mut cfg);
19-
}
20-
21-
// Reverse direction
22-
let poly = polyline_reverse(&poly);
23-
for i in 1..500 {
24-
offset_polyline_to_polyline(&poly, (i as f64)/5.0, &mut cfg);
15+
16+
for _ in 0..100 {
17+
// Forward direction
18+
for i in 0..100 {
19+
offset_polyline_to_polyline(&poly, (i as f64) / 5.0, &mut cfg);
20+
}
21+
22+
// Reverse direction
23+
let poly = polyline_reverse(&poly);
24+
for i in 0..100 {
25+
offset_polyline_to_polyline(&poly, (i as f64) / 5.0, &mut cfg);
26+
}
2527
}
26-
28+
2729
let total_time = start.elapsed();
28-
let operations = 149 * 2; // 149 offsets in each direction
29-
let avg_per_operation = total_time / operations;
30-
31-
println!("Total time for {} offset operations: {:?}", operations, total_time);
30+
let operations = 100 * 100 * 2; // 100 external iterations × 100 offsets forward × 100 offsets reverse
31+
let avg_per_operation = total_time / operations as u32;
32+
33+
println!(
34+
"Total time for {} offset operations: {:?}",
35+
operations, total_time
36+
);
3237
println!("Average time per operation: {:?}", avg_per_operation);
33-
println!("Operations per second: {:.1}", 1.0 / avg_per_operation.as_secs_f64());
38+
println!(
39+
"Operations per second: {:.1}",
40+
1.0 / avg_per_operation.as_secs_f64()
41+
);
3442
}
3543

3644
/*
37-
>
45+
>
3846
cargo bench --bench bench_offset_multiple
3947
4048
Base:
@@ -52,10 +60,15 @@ Total time for 298 offset operations: 185.851519ms
5260
Average time per operation: 623.662µs
5361
Operations per second: 1603.4
5462
_____________________________________________________
55-
Opt 10 - using aabb check before split
63+
Opt 10 - using aabb check before split
5664
Total time for 298 offset operations: 143.272175ms
5765
Average time per operation: 480.779µs
5866
Operations per second: 2080.0
5967
_____________________________________________________
68+
Opt 11 - however the benchmars was changed to 0..100 from 0..500 and external loop added
69+
Total time for 20000 offset operations: 1.407260869s
70+
Average time per operation: 70.363µs
71+
Operations per second: 14212.0
72+
_____________________________________________________
6073
6174
*/

benches/bench_offset_multiple1000.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ Total time for 50 offset operations: 2.359176736s
6666
Average time per operation: 47.183534ms
6767
Operations per second: 21.2
6868
_________________________________________________________
69-
70-
69+
Opt 12 - build_graph() with spatial index (54%)
70+
Total time for 50 offset operations: 1.113952202s
71+
Average time per operation: 22.279044ms
72+
Operations per second: 44.9
73+
_________________________________________________________
7174
*/

benches/bench_offset_multiple200.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,9 @@ Total time for 1980 offset operations: 6.082281414s
8181
Average time per operation: 3.071859ms
8282
Operations per second: 325.5
8383
__________________________________________________________
84+
Opt 12 - build_graph() with spatial index (34%)
85+
Total time for 1980 offset operations: 4.004032514s
86+
Average time per operation: 2.022238ms
87+
Operations per second: 494.5
88+
__________________________________________________________
8489
*/

benches/bench_offset_multiple500.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,9 @@ Total time for 198 offset operations: 2.298718087s
6666
Average time per operation: 11.609687ms
6767
Operations per second: 86.1
6868
_______________________________________________________________
69-
69+
Opt 12 - build_graph() with spatial index (30%)
70+
Total time for 198 offset operations: 1.655199977s
71+
Average time per operation: 8.359595ms
72+
Operations per second: 119.6
73+
_______________________________________________________________
7074
*/

examples/offset_pline1.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ fn main() {
1313
// Translate to fit in the SVG viewport
1414
let poly = polyline_translate(&poly_orig, point(100.0, -50.0));
1515

16-
let offset_polylines = offset_polyline_to_polyline(&poly, 10.0, &mut cfg);
16+
let offset_polylines = offset_polyline_to_polyline(&poly, 16.0-1e-9, &mut cfg);
1717
// Internal offsetting
1818
let poly = polyline_reverse(&poly);
1919
let offset_polylines2 = offset_polyline_to_polyline(&poly, 15.5600615, &mut cfg);
@@ -26,7 +26,7 @@ fn main() {
2626
}
2727

2828
assert_eq!(offset_polylines.len(), 1, "Expected exactly 1 offset polyline");
29-
assert_eq!(offset_polylines[0].len(), 27);
29+
assert_eq!(offset_polylines[0].len(), 23);
3030
assert_eq!(offset_polylines2.len(), 4, "Expected exactly 1 offset polyline");
3131
assert_eq!(offset_polylines2[0].len(), 9);
3232
assert_eq!(offset_polylines2[1].len(), 3);

src/graph/find_cycles.rs

Lines changed: 110 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
//! 4. Return non-intersecting cycles as separate arc sequences
1515
1616
use togo::prelude::*;
17-
use std::collections::{HashMap, HashSet};
17+
use std::collections::HashMap;
18+
use aabb::HilbertRTree;
1819

1920
/// Tolerance for considering vertices as the same point
2021
const VERTEX_TOLERANCE: f64 = 1e-8;
@@ -45,6 +46,9 @@ struct CycleGraph {
4546
adjacency: HashMap<VertexId, Vec<usize>>,
4647
/// All edges in the graph
4748
edges: Vec<GraphEdge>,
49+
/// Spatial index for fast vertex lookup
50+
#[allow(dead_code)]
51+
vertex_spatial_index: Option<HilbertRTree>,
4852
}
4953

5054
impl CycleGraph {
@@ -54,19 +58,14 @@ impl CycleGraph {
5458
vertices: Vec::new(),
5559
adjacency: HashMap::new(),
5660
edges: Vec::new(),
61+
vertex_spatial_index: None,
5762
}
5863
}
5964

60-
/// Add or find vertex for a given point, merging close points
61-
fn add_vertex(&mut self, point: Point) -> VertexId {
62-
// Check if point is close to existing vertex
63-
for (i, existing_point) in self.vertices.iter().enumerate() {
64-
if (point - *existing_point).norm() < VERTEX_TOLERANCE {
65-
return VertexId(i);
66-
}
67-
}
68-
69-
// Add new vertex
65+
/// Add vertex without merging - just collect all endpoints
66+
/// Merging happens later in build_graph using spatial index
67+
fn add_vertex_raw(&mut self, point: Point) -> VertexId {
68+
// Always add, no filtering - merging done post-build
7069
let vertex_id = VertexId(self.vertices.len());
7170
self.vertices.push(point);
7271
self.adjacency.insert(vertex_id, Vec::new());
@@ -75,8 +74,8 @@ impl CycleGraph {
7574

7675
/// Add an edge to the graph
7776
fn add_edge(&mut self, arc: Arc) {
78-
let from = self.add_vertex(arc.a);
79-
let to = self.add_vertex(arc.b);
77+
let from = self.add_vertex_raw(arc.a);
78+
let to = self.add_vertex_raw(arc.b);
8079

8180
let edge = GraphEdge {
8281
arc,
@@ -104,14 +103,95 @@ impl CycleGraph {
104103
}
105104

106105
/// Build graph from input arcs
106+
/// Build graph from input arcs with optional spatial-indexed vertex merging.
107+
/// Merging is done here to handle cases where find_non_intersecting_cycles is called
108+
/// without prior merge_close_endpoints, but the overhead is minimal when vertices are
109+
/// already merged (most vertices map to themselves).
107110
fn build_graph(arcs: &[Arc]) -> CycleGraph {
108111
let mut graph = CycleGraph::new();
109112

113+
// Pass 1: Add all arcs and collect vertices
110114
for arc in arcs {
111115
graph.add_edge(*arc);
112116
}
113117

114-
graph
118+
if graph.vertices.is_empty() {
119+
return graph;
120+
}
121+
122+
// Pass 2: Build spatial index of all vertices
123+
let mut spatial_index = HilbertRTree::with_capacity(graph.vertices.len());
124+
for point in &graph.vertices {
125+
spatial_index.add_point(point.x, point.y);
126+
}
127+
spatial_index.build();
128+
129+
// Pass 3: Find and merge close vertices using spatial queries
130+
let mut vertex_mapping: HashMap<usize, usize> = HashMap::new(); // old_id -> new_id
131+
let mut merged_vertices: Vec<Point> = Vec::with_capacity(graph.vertices.len());
132+
let mut used = vec![false; graph.vertices.len()];
133+
let mut nearby_indices: Vec<usize> = Vec::with_capacity(graph.vertices.len() / 8); // Preallocate reusable buffer
134+
135+
for i in 0..graph.vertices.len() {
136+
if used[i] {
137+
continue;
138+
}
139+
140+
let point_i = graph.vertices[i];
141+
142+
// Find all nearby vertices using spatial index
143+
nearby_indices.clear();
144+
spatial_index.query_circle(point_i.x, point_i.y, VERTEX_TOLERANCE, &mut nearby_indices);
145+
146+
// Keep the first one, merge others into it
147+
let new_vertex_id = merged_vertices.len();
148+
merged_vertices.push(point_i);
149+
vertex_mapping.insert(i, new_vertex_id);
150+
used[i] = true;
151+
152+
// Merge nearby vertices (already filtered by query_circle, no need to check distance again)
153+
for &nearby_idx in &nearby_indices {
154+
if nearby_idx != i && !used[nearby_idx] {
155+
vertex_mapping.insert(nearby_idx, new_vertex_id);
156+
used[nearby_idx] = true;
157+
}
158+
}
159+
}
160+
161+
// Pass 4: Rebuild graph with merged vertices
162+
let mut new_graph = CycleGraph::new();
163+
new_graph.vertices = merged_vertices;
164+
new_graph.edges.reserve(graph.edges.len());
165+
166+
// Initialize adjacency lists for new vertices
167+
for i in 0..new_graph.vertices.len() {
168+
new_graph.adjacency.insert(VertexId(i), Vec::new());
169+
}
170+
171+
// Remap edges to use new vertex IDs
172+
for edge in &graph.edges {
173+
let old_from = edge.from.0;
174+
let old_to = edge.to.0;
175+
176+
// Look up merged vertex IDs - most vertices map to themselves if not merged
177+
let new_from_id = *vertex_mapping.get(&old_from).unwrap_or(&old_from);
178+
let new_to_id = *vertex_mapping.get(&old_to).unwrap_or(&old_to);
179+
let new_from = VertexId(new_from_id);
180+
let new_to = VertexId(new_to_id);
181+
182+
let remapped_edge = GraphEdge {
183+
arc: edge.arc,
184+
from: new_from,
185+
to: new_to,
186+
id: new_graph.edges.len(),
187+
};
188+
189+
new_graph.adjacency.get_mut(&new_from).unwrap().push(remapped_edge.id);
190+
new_graph.adjacency.get_mut(&new_to).unwrap().push(remapped_edge.id);
191+
new_graph.edges.push(remapped_edge);
192+
}
193+
194+
new_graph
115195
}
116196

117197
/// Find the next edge to follow from current vertex, avoiding the edge we came from
@@ -120,17 +200,18 @@ fn find_next_edge(
120200
graph: &CycleGraph,
121201
current_vertex: VertexId,
122202
came_from_edge: Option<usize>,
123-
used_edges: &HashSet<usize>
203+
used_edges: &[bool],
204+
cycle_edges: &[usize]
124205
) -> Option<usize> {
125206
let adjacent_edges = graph.get_adjacent_edges(current_vertex);
126207

127-
// Filter out the edge we came from and already used edges
128-
let available_edges: Vec<usize> = adjacent_edges.iter()
129-
.copied()
130-
.filter(|&edge_id| {
131-
Some(edge_id) != came_from_edge && !used_edges.contains(&edge_id)
132-
})
133-
.collect();
208+
// Filter out the edge we came from, already used edges, and edges in current cycle
209+
let mut available_edges: Vec<usize> = Vec::with_capacity(adjacent_edges.len());
210+
for &edge_id in adjacent_edges {
211+
if Some(edge_id) != came_from_edge && !used_edges[edge_id] && !cycle_edges.contains(&edge_id) {
212+
available_edges.push(edge_id);
213+
}
214+
}
134215

135216
if available_edges.is_empty() {
136217
return None;
@@ -165,7 +246,7 @@ fn choose_rightmost_edge(
165246
let incoming_direction = get_arc_direction_at_vertex(&incoming_edge.arc, vertex_pos, true);
166247

167248
// Calculate angles for all available outgoing edges
168-
let mut edge_angles: Vec<(usize, f64)> = Vec::new();
249+
let mut edge_angles: Vec<(usize, f64)> = Vec::with_capacity(available_edges.len());
169250

170251
for &edge_id in available_edges {
171252
let edge = &graph.edges[edge_id];
@@ -251,9 +332,9 @@ fn get_arc_direction_at_vertex(arc: &Arc, vertex_pos: Point, incoming: bool) ->
251332
fn find_cycle_from_edge(
252333
graph: &CycleGraph,
253334
start_edge_id: usize,
254-
used_edges: &mut HashSet<usize>
335+
used_edges: &mut Vec<bool>
255336
) -> Option<Vec<Arc>> {
256-
if used_edges.contains(&start_edge_id) {
337+
if used_edges[start_edge_id] {
257338
return None;
258339
}
259340

@@ -270,7 +351,7 @@ fn find_cycle_from_edge(
270351
if current_vertex == start_vertex {
271352
// Mark all edges in this cycle as used
272353
for edge_id in &cycle_edges {
273-
used_edges.insert(*edge_id);
354+
used_edges[*edge_id] = true;
274355
}
275356

276357
// Convert edge IDs to arcs
@@ -281,14 +362,8 @@ fn find_cycle_from_edge(
281362
return Some(cycle_arcs);
282363
}
283364

284-
// Create a temporary set of edges to avoid in this search (current path + permanently used)
285-
let mut temp_used: HashSet<usize> = used_edges.clone();
286-
for &edge_id in &cycle_edges {
287-
temp_used.insert(edge_id);
288-
}
289-
290-
// Find next edge to follow
291-
if let Some(next_edge_id) = find_next_edge(graph, current_vertex, Some(current_edge_id), &temp_used) {
365+
// Find next edge to follow (create temporary tracking on the fly)
366+
if let Some(next_edge_id) = find_next_edge(graph, current_vertex, Some(current_edge_id), used_edges, &cycle_edges) {
292367
let next_edge = &graph.edges[next_edge_id];
293368

294369
// Determine which vertex to move to
@@ -316,7 +391,7 @@ pub fn find_non_intersecting_cycles(arcs: &[Arc]) -> Vec<Vec<Arc>> {
316391
let graph = build_graph(arcs);
317392

318393
let mut cycles = Vec::new();
319-
let mut used_edges = HashSet::new();
394+
let mut used_edges = vec![false; graph.edges.len()]; // Use Vec<bool> instead of HashSet
320395

321396
// Try to find cycles starting from each edge
322397
for edge_id in 0..graph.edges.len() {

0 commit comments

Comments
 (0)