Spatial Data Structures
This example demonstrates spatial data structures for efficient queries.
Overview
Spatial data structures enable fast nearest-neighbor and range queries:
KD-Tree: k-dimensional binary search tree
R-Tree: Rectangle tree for bounding box queries
Ball Tree: Metric tree for arbitrary metrics
Cover Tree: Efficient for intrinsic dimensionality
Key Concepts
Nearest neighbor queries: Find k closest points
Range queries: Find all points within radius
Bulk loading: Efficient tree construction
Metric spaces: Distance-based operations
Data Structures
- KD-Tree
Best for low-dimensional data (d < 20)
O(log n) average query time
Standard Euclidean distance
- R-Tree
Designed for spatial indexing
Handles bounding boxes well
Good for GIS applications
- Ball Tree
Works in any metric space
Better for high dimensions
Supports custom distance functions
Code Highlights
The example demonstrates:
Building KD-tree with
KDTree()Nearest neighbor queries with
query()Range queries with
query_radius()Bulk operations for efficiency
Source Code
1"""
2Spatial Data Structures Example
3===============================
4
5This example demonstrates spatial data structures in PyTCL for
6efficient nearest neighbor queries and spatial indexing:
7
8K-D Tree:
9- Construction and querying
10- K-nearest neighbor search
11- Radius/range queries
12
13Ball Tree:
14- Alternative to K-D tree for high dimensions
15- Similar query interface
16
17R-Tree:
18- Spatial indexing for bounding boxes
19- Rectangle intersection queries
20
21VP-Tree (Vantage Point Tree):
22- Metric space indexing
23- Works with any distance metric
24
25Cover Tree:
26- Approximate nearest neighbor search
27- O(c^12 log n) query complexity
28
29These data structures are essential for efficient data association
30in multi-target tracking and spatial analysis applications.
31"""
32
33from pathlib import Path
34
35import numpy as np
36import plotly.graph_objects as go
37
38# Output directory for generated plots
39OUTPUT_DIR = Path(__file__).parent.parent / "docs" / "_static" / "images" / "examples"
40OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
41
42# Global flag to control plotting
43SHOW_PLOTS = True
44
45
46from pytcl.containers import ( # K-D Tree; Ball Tree; R-Tree; VP-Tree; Cover Tree
47 BallTree,
48 BoundingBox,
49 CoverTree,
50 KDTree,
51 NearestNeighborResult,
52 RTree,
53 VPTree,
54 box_from_point,
55 box_from_points,
56 merge_boxes,
57)
58
59
60def demo_kdtree_basics():
61 """Demonstrate K-D tree construction and basic queries."""
62 print("=" * 70)
63 print("K-D Tree Basics Demo")
64 print("=" * 70)
65
66 np.random.seed(42)
67
68 # Generate 2D point cloud
69 n_points = 100
70 points = np.random.randn(n_points, 2) * 10
71
72 print(f"\nBuilding K-D tree with {n_points} 2D points")
73
74 # Build tree
75 tree = KDTree(points)
76
77 print(f"Tree built successfully")
78 print(f"Point cloud bounds:")
79 print(f" X: [{points[:, 0].min():.2f}, {points[:, 0].max():.2f}]")
80 print(f" Y: [{points[:, 1].min():.2f}, {points[:, 1].max():.2f}]")
81
82 # Query point
83 query = np.array([0.0, 0.0])
84 print(f"\nQuery point: {query}")
85
86 # K-nearest neighbors
87 k = 5
88 result = tree.query(query, k=k)
89
90 print(f"\n{k} nearest neighbors:")
91 # indices/distances are 2D arrays, extract the first row for single query
92 for i, (idx, dist) in enumerate(zip(result.indices[0], result.distances[0])):
93 print(f" {i + 1}. Point {idx}: {points[idx]} (distance={dist:.4f})")
94
95 # Plot KDTree result
96 if SHOW_PLOTS:
97 fig = go.Figure()
98
99 # All points
100 fig.add_trace(
101 go.Scatter(
102 x=points[:, 0],
103 y=points[:, 1],
104 mode="markers",
105 marker=dict(color="lightblue", size=8, opacity=0.6),
106 name="Points",
107 )
108 )
109
110 # K nearest neighbors
111 nn_indices = result.indices[0]
112 fig.add_trace(
113 go.Scatter(
114 x=points[nn_indices, 0],
115 y=points[nn_indices, 1],
116 mode="markers",
117 marker=dict(color="green", size=12, opacity=0.8),
118 name=f"{k} nearest neighbors",
119 )
120 )
121
122 # Query point
123 fig.add_trace(
124 go.Scatter(
125 x=[query[0]],
126 y=[query[1]],
127 mode="markers",
128 marker=dict(color="red", size=15, symbol="star"),
129 name="Query",
130 )
131 )
132
133 # Draw circle for max distance
134 max_dist = result.distances[0, -1]
135 theta = np.linspace(0, 2 * np.pi, 100)
136 circle_x = query[0] + max_dist * np.cos(theta)
137 circle_y = query[1] + max_dist * np.sin(theta)
138 fig.add_trace(
139 go.Scatter(
140 x=circle_x,
141 y=circle_y,
142 mode="lines",
143 line=dict(color="green", dash="dash", width=2),
144 name="Search radius",
145 showlegend=True,
146 )
147 )
148
149 fig.update_layout(
150 title="K-D Tree: K-Nearest Neighbor Query",
151 xaxis_title="x",
152 yaxis_title="y",
153 height=600,
154 width=600,
155 showlegend=True,
156 xaxis=dict(scaleanchor="y", scaleratio=1),
157 )
158 fig.write_html(
159 str(OUTPUT_DIR / "spatial_kdtree.html"),
160 include_plotlyjs="cdn",
161 div_id="spatial_kdtree",
162 )
163 print("\n [Plot saved to spatial_kdtree.html]")
164
165
166def demo_kdtree_queries():
167 """Demonstrate K-D tree query types."""
168 print("\n" + "=" * 70)
169 print("K-D Tree Query Types Demo")
170 print("=" * 70)
171
172 np.random.seed(42)
173
174 # Create structured point cloud
175 # Grid + noise
176 x = np.linspace(-10, 10, 11)
177 y = np.linspace(-10, 10, 11)
178 xx, yy = np.meshgrid(x, y)
179 points = np.column_stack([xx.ravel(), yy.ravel()])
180 points += np.random.randn(*points.shape) * 0.3
181
182 tree = KDTree(points)
183
184 print(f"\nGrid-based point cloud: {len(points)} points")
185
186 # Different query types
187 query = np.array([0.0, 0.0])
188
189 # K-NN query
190 k_values = [1, 5, 10, 20]
191 print("\n--- K-Nearest Neighbors ---")
192 for k in k_values:
193 result = tree.query(query, k=k)
194 max_dist = result.distances[0, -1] # 2D array: [query_idx, neighbor_idx]
195 print(f" k={k:>2}: max distance = {max_dist:.4f}")
196
197 # Radius query
198 print("\n--- Radius Queries ---")
199 radii = [1.0, 2.0, 5.0, 10.0]
200 for r in radii:
201 result = tree.query_radius(query, r)
202 # query_radius returns list of index arrays (one per query point)
203 print(f" radius={r:.1f}: {len(result[0])} points found")
204
205
206def demo_balltree():
207 """Demonstrate Ball Tree for higher dimensions."""
208 print("\n" + "=" * 70)
209 print("Ball Tree Demo")
210 print("=" * 70)
211
212 np.random.seed(42)
213
214 # Higher dimensional data
215 n_points = 500
216 n_dims = 10 # 10-dimensional space
217
218 points = np.random.randn(n_points, n_dims)
219
220 print(f"\nBuilding Ball Tree with {n_points} points in {n_dims}D space")
221
222 tree = BallTree(points)
223
224 # Query
225 query = np.zeros(n_dims)
226 k = 5
227
228 result = tree.query(query, k=k)
229
230 print(f"\nQuery: origin in {n_dims}D")
231 print(f"{k} nearest neighbors:")
232 for i, (idx, dist) in enumerate(zip(result.indices[0], result.distances[0])):
233 print(f" {i + 1}. Point {idx}: distance = {dist:.4f}")
234
235 print("\nNote: Ball Tree is often more efficient than K-D Tree")
236 print("for higher dimensional data (curse of dimensionality).")
237
238
239def demo_rtree():
240 """Demonstrate R-Tree for bounding box indexing."""
241 print("\n" + "=" * 70)
242 print("R-Tree Demo")
243 print("=" * 70)
244
245 np.random.seed(42)
246
247 # Create bounding boxes (e.g., for spatial objects)
248 n_boxes = 50
249 boxes = []
250
251 for i in range(n_boxes):
252 # Random center and size
253 center = np.random.uniform(-50, 50, 2)
254 size = np.random.uniform(2, 10, 2)
255
256 min_coords = center - size / 2
257 max_coords = center + size / 2
258
259 box = BoundingBox(min_coords=min_coords, max_coords=max_coords)
260 boxes.append(box)
261
262 print(f"\nCreated {n_boxes} bounding boxes")
263
264 # Build R-Tree
265 tree = RTree()
266 for i, box in enumerate(boxes):
267 tree.insert(box, i)
268
269 print("R-Tree built successfully")
270
271 # Query: find boxes intersecting a search region
272 search_min = np.array([-10, -10])
273 search_max = np.array([10, 10])
274 search_box = BoundingBox(min_coords=search_min, max_coords=search_max)
275
276 print(f"\nSearch region: ({search_min} to {search_max})")
277
278 result = tree.query_intersect(search_box)
279
280 print(f"Found {len(result.indices)} intersecting boxes")
281
282 # Show some results
283 if len(result.indices) > 0:
284 print("\nFirst 5 intersecting boxes:")
285 for idx in result.indices[:5]:
286 box = boxes[idx]
287 print(f" Box {idx}: ({box.min_coords} to {box.max_coords})")
288
289 # Plot R-Tree result
290 if SHOW_PLOTS:
291 fig = go.Figure()
292
293 # Draw all boxes
294 for i, box in enumerate(boxes):
295 is_intersecting = i in result.indices
296 color = "green" if is_intersecting else "lightblue"
297 opacity = 0.6 if is_intersecting else 0.3
298
299 # Create rectangle as a filled shape
300 x0, y0 = box.min_coords
301 x1, y1 = box.max_coords
302
303 fig.add_shape(
304 type="rect",
305 x0=x0,
306 y0=y0,
307 x1=x1,
308 y1=y1,
309 fillcolor=color,
310 line=dict(color="black", width=1),
311 opacity=opacity,
312 )
313
314 # Draw search region
315 fig.add_shape(
316 type="rect",
317 x0=search_min[0],
318 y0=search_min[1],
319 x1=search_max[0],
320 y1=search_max[1],
321 fillcolor="rgba(0,0,0,0)",
322 line=dict(color="red", width=3, dash="dash"),
323 )
324
325 # Add legend traces (invisible points for legend)
326 fig.add_trace(
327 go.Scatter(
328 x=[None],
329 y=[None],
330 mode="markers",
331 marker=dict(size=15, color="green", opacity=0.6),
332 name="Intersecting",
333 )
334 )
335 fig.add_trace(
336 go.Scatter(
337 x=[None],
338 y=[None],
339 mode="markers",
340 marker=dict(size=15, color="lightblue", opacity=0.3),
341 name="Non-intersecting",
342 )
343 )
344 fig.add_trace(
345 go.Scatter(
346 x=[None],
347 y=[None],
348 mode="lines",
349 line=dict(color="red", width=3, dash="dash"),
350 name="Search region",
351 )
352 )
353
354 fig.update_layout(
355 title=f"R-Tree: {len(result.indices)} boxes intersecting search region",
356 xaxis_title="x",
357 yaxis_title="y",
358 height=700,
359 width=700,
360 showlegend=True,
361 xaxis=dict(range=[-60, 60], scaleanchor="y", scaleratio=1),
362 yaxis=dict(range=[-60, 60]),
363 )
364 fig.write_html(
365 str(OUTPUT_DIR / "spatial_rtree.html"),
366 include_plotlyjs="cdn",
367 div_id="spatial_rtree",
368 )
369 print("\n [Plot saved to spatial_rtree.html]")
370
371
372def demo_bounding_box_operations():
373 """Demonstrate bounding box utility functions."""
374 print("\n" + "=" * 70)
375 print("Bounding Box Operations Demo")
376 print("=" * 70)
377
378 # Create boxes
379 box1 = BoundingBox(min_coords=np.array([0, 0]), max_coords=np.array([5, 5]))
380
381 box2 = BoundingBox(min_coords=np.array([3, 3]), max_coords=np.array([8, 8]))
382
383 box3 = BoundingBox(min_coords=np.array([10, 10]), max_coords=np.array([12, 12]))
384
385 print("\nBox 1: (0,0) to (5,5)")
386 print(f" Center: {box1.center}")
387 print(f" Dimensions: {box1.dimensions}")
388 print(f" Volume: {box1.volume}")
389
390 print("\nBox 2: (3,3) to (8,8)")
391 print(f" Center: {box2.center}")
392
393 print("\nBox 3: (10,10) to (12,12)")
394 print(f" Center: {box3.center}")
395
396 # Intersection tests
397 print("\n--- Intersection Tests ---")
398 print(f" Box1 intersects Box2: {box1.intersects(box2)}")
399 print(f" Box1 intersects Box3: {box1.intersects(box3)}")
400 print(f" Box2 intersects Box3: {box2.intersects(box3)}")
401
402 # Point containment
403 test_points = [
404 np.array([2.5, 2.5]),
405 np.array([4.0, 4.0]),
406 np.array([7.0, 7.0]),
407 ]
408
409 print("\n--- Point Containment Tests ---")
410 for p in test_points:
411 print(f" Point {p}:")
412 print(f" In Box1: {box1.contains_point(p)}")
413 print(f" In Box2: {box2.contains_point(p)}")
414
415 # Merge boxes
416 merged = merge_boxes([box1, box2]) # Takes a list of boxes
417 print("\n--- Merged Box (Box1 + Box2) ---")
418 print(f" Min: {merged.min_coords}")
419 print(f" Max: {merged.max_coords}")
420
421 # Create box from points
422 points = np.array([[1, 2], [5, 3], [2, 8], [7, 4]])
423 bbox = box_from_points(points)
424 print("\n--- Bounding Box of Points ---")
425 print(f" Points:\n{points}")
426 print(f" Bounding box: ({bbox.min_coords} to {bbox.max_coords})")
427
428
429def demo_vptree():
430 """Demonstrate VP-Tree for metric space indexing."""
431 print("\n" + "=" * 70)
432 print("VP-Tree Demo")
433 print("=" * 70)
434
435 np.random.seed(42)
436
437 # Generate points
438 n_points = 200
439 points = np.random.randn(n_points, 3) * 5
440
441 print(f"\nBuilding VP-Tree with {n_points} 3D points")
442
443 tree = VPTree(points)
444
445 # Query
446 query = np.array([1.0, 1.0, 1.0])
447 k = 5
448
449 result = tree.query(query, k=k)
450
451 print(f"\nQuery point: {query}")
452 print(f"{k} nearest neighbors:")
453 for i, (idx, dist) in enumerate(zip(result.indices[0], result.distances[0])):
454 print(f" {i + 1}. Point {idx}: distance = {dist:.4f}")
455
456 print("\nNote: VP-Tree works with any distance metric,")
457 print("not just Euclidean distance.")
458
459
460def demo_covertree():
461 """Demonstrate Cover Tree for approximate nearest neighbor."""
462 print("\n" + "=" * 70)
463 print("Cover Tree Demo")
464 print("=" * 70)
465
466 np.random.seed(42)
467
468 # Generate points
469 n_points = 300
470 points = np.random.randn(n_points, 4) * 3 # 4D
471
472 print(f"\nBuilding Cover Tree with {n_points} 4D points")
473
474 tree = CoverTree(points)
475
476 # Query
477 query = np.zeros(4)
478 k = 5
479
480 result = tree.query(query, k=k)
481
482 print(f"\nQuery: origin in 4D")
483 print(f"{k} nearest neighbors:")
484 for i, (idx, dist) in enumerate(zip(result.indices[0], result.distances[0])):
485 print(f" {i + 1}. Point {idx}: distance = {dist:.4f}")
486
487 print("\nNote: Cover Tree provides O(c^12 log n) query complexity")
488 print("where c is the expansion constant of the data.")
489
490
491def demo_performance_comparison():
492 """Compare performance of different spatial data structures."""
493 print("\n" + "=" * 70)
494 print("Performance Comparison Demo")
495 print("=" * 70)
496
497 import time
498
499 np.random.seed(42)
500
501 # Test data
502 n_points = 5000
503 n_queries = 100
504 dims = 3
505 k = 10
506
507 points = np.random.randn(n_points, dims) * 10
508 queries = np.random.randn(n_queries, dims) * 10
509
510 print(f"\nDataset: {n_points} points in {dims}D")
511 print(f"Queries: {n_queries} k-NN queries (k={k})")
512
513 results = {}
514
515 # K-D Tree
516 t0 = time.time()
517 kdtree = KDTree(points)
518 build_time = time.time() - t0
519
520 t0 = time.time()
521 for q in queries:
522 kdtree.query(q, k=k)
523 query_time = time.time() - t0
524
525 results["K-D Tree"] = (build_time, query_time)
526
527 # Ball Tree
528 t0 = time.time()
529 balltree = BallTree(points)
530 build_time = time.time() - t0
531
532 t0 = time.time()
533 for q in queries:
534 balltree.query(q, k=k)
535 query_time = time.time() - t0
536
537 results["Ball Tree"] = (build_time, query_time)
538
539 # VP Tree
540 t0 = time.time()
541 vptree = VPTree(points)
542 build_time = time.time() - t0
543
544 t0 = time.time()
545 for q in queries:
546 vptree.query(q, k=k)
547 query_time = time.time() - t0
548
549 results["VP-Tree"] = (build_time, query_time)
550
551 # Cover Tree
552 t0 = time.time()
553 covertree = CoverTree(points)
554 build_time = time.time() - t0
555
556 t0 = time.time()
557 for q in queries:
558 covertree.query(q, k=k)
559 query_time = time.time() - t0
560
561 results["Cover Tree"] = (build_time, query_time)
562
563 # Print results
564 print("\n" + "-" * 50)
565 print(f"{'Structure':<15} {'Build (ms)':>12} {'Query (ms)':>12}")
566 print("-" * 50)
567 for name, (build, query) in results.items():
568 print(f"{name:<15} {build * 1000:>12.2f} {query * 1000:>12.2f}")
569
570 print("\nNote: Performance depends on data distribution and dimensionality.")
571
572
573def demo_tracking_application():
574 """Demonstrate spatial indexing in tracking context."""
575 print("\n" + "=" * 70)
576 print("Tracking Application Demo")
577 print("=" * 70)
578
579 np.random.seed(42)
580
581 # Simulated scenario: sensor provides measurements,
582 # need to associate with predicted track positions
583
584 # Track predictions
585 n_tracks = 20
586 track_positions = np.random.uniform(-100, 100, (n_tracks, 2))
587
588 # Measurements (some from tracks, some false alarms)
589 n_measurements = 30
590 # First n_tracks measurements near track positions
591 measurements = np.zeros((n_measurements, 2))
592 for i in range(min(n_tracks, n_measurements)):
593 measurements[i] = track_positions[i] + np.random.randn(2) * 2.0
594
595 # Remaining are false alarms
596 for i in range(n_tracks, n_measurements):
597 measurements[i] = np.random.uniform(-100, 100, 2)
598
599 print(f"\n{n_tracks} track predictions")
600 print(
601 f"{n_measurements} measurements ({n_tracks} true + "
602 f"{n_measurements - n_tracks} false alarms)"
603 )
604
605 # Build spatial index on track predictions
606 tree = KDTree(track_positions)
607
608 # For each measurement, find nearest track
609 print("\nMeasurement-to-track association using K-D tree:")
610 print("-" * 50)
611
612 gating_threshold = 5.0 # meters
613 associations = []
614
615 for m_idx, meas in enumerate(measurements):
616 result = tree.query(meas, k=1)
617 nearest_track = result.indices[0, 0] # 2D array [query_idx, neighbor_idx]
618 distance = result.distances[0, 0]
619
620 if distance < gating_threshold:
621 associations.append((m_idx, nearest_track, distance))
622
623 print(f"Gating threshold: {gating_threshold} m")
624 print(f"Measurements passing gate: {len(associations)}/{n_measurements}")
625
626 # Show some associations
627 print("\nFirst 5 associations:")
628 for m_idx, t_idx, dist in associations[:5]:
629 true_assoc = m_idx == t_idx # Simplified ground truth
630 status = "+" if true_assoc else "?"
631 print(f" Meas {m_idx:>2} -> Track {t_idx:>2} (dist={dist:.2f}) {status}")
632
633 # Radius query for gating
634 print("\n--- Using Radius Query for Gating ---")
635 meas_test = measurements[0]
636 result = tree.query_radius(meas_test, gating_threshold)
637 # query_radius returns list of index arrays (one per query point)
638 print(f"Measurement 0: {len(result[0])} tracks within gate")
639
640
641def main():
642 """Run all demonstrations."""
643 print("\n" + "#" * 70)
644 print("# PyTCL Spatial Data Structures Example")
645 print("#" * 70)
646
647 demo_kdtree_basics()
648 demo_kdtree_queries()
649 demo_balltree()
650 demo_rtree()
651 demo_bounding_box_operations()
652 demo_vptree()
653 demo_covertree()
654 demo_performance_comparison()
655 demo_tracking_application()
656
657 print("\n" + "=" * 70)
658 print("Example complete!")
659 if SHOW_PLOTS:
660 print("Plots saved: spatial_kdtree.html, spatial_rtree.html")
661 print("=" * 70)
662
663
664if __name__ == "__main__":
665 main()
Running the Example
python examples/spatial_data_structures.py
See Also
Gaussian Mixtures and Clustering - Clustering with spatial trees
Multi-Target Tracking - Gating with spatial queries