fastquadtree 0.5.0__tar.gz → 0.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/.pre-commit-config.yaml +11 -9
  2. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/Cargo.lock +1 -1
  3. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/Cargo.toml +1 -1
  4. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/PKG-INFO +10 -2
  5. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/README.md +2 -1
  6. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/benchmarks/benchmark_native_vs_shim.py +25 -6
  7. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/benchmarks/quadtree_bench/__init__.py +2 -2
  8. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/benchmarks/quadtree_bench/engines.py +21 -8
  9. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/benchmarks/quadtree_bench/main.py +1 -1
  10. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/benchmarks/quadtree_bench/plotting.py +24 -24
  11. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/benchmarks/quadtree_bench/runner.py +82 -12
  12. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/benchmarks/runner.py +0 -7
  13. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/interactive/interactive.py +12 -6
  14. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/interactive/interactive_v2.py +123 -95
  15. fastquadtree-0.5.1/pyproject.toml +130 -0
  16. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/pysrc/fastquadtree/__init__.py +42 -45
  17. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/pysrc/fastquadtree/__init__.pyi +25 -21
  18. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/pysrc/fastquadtree/_bimap.py +12 -11
  19. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/pysrc/fastquadtree/_item.py +5 -4
  20. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/tests/test_bimap.py +7 -6
  21. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/tests/test_delete_by_object.py +1 -0
  22. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/tests/test_delete_python.py +0 -1
  23. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/tests/test_python.py +7 -4
  24. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/tests/test_unconventional_bounds.py +4 -2
  25. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/tests/test_wrapper_edges.py +14 -15
  26. fastquadtree-0.5.0/pyproject.toml +0 -56
  27. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/.github/workflows/release.yml +0 -0
  28. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/.gitignore +0 -0
  29. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/LICENSE +0 -0
  30. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/assets/interactive_v2_screenshot.png +0 -0
  31. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/assets/quadtree_bench_throughput.png +0 -0
  32. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/assets/quadtree_bench_time.png +0 -0
  33. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/benchmarks/cross_library_bench.py +0 -0
  34. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/benchmarks/requirements.txt +0 -0
  35. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/interactive/requirements.txt +0 -0
  36. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/pysrc/fastquadtree/py.typed +0 -0
  37. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/src/geom.rs +0 -0
  38. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/src/lib.rs +0 -0
  39. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/src/quadtree.rs +0 -0
  40. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/tests/insertions.rs +0 -0
  41. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/tests/nearest_neighbor.rs +0 -0
  42. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/tests/query.rs +0 -0
  43. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/tests/rectangle_traversal.rs +0 -0
  44. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/tests/test_delete.rs +0 -0
  45. {fastquadtree-0.5.0 → fastquadtree-0.5.1}/tests/unconventional_bounds.rs +0 -0
@@ -1,12 +1,15 @@
1
1
  repos:
2
- # 0) Python formatter: black
3
- - repo: https://github.com/psf/black
4
- rev: 24.8.0
2
+ # 0) Python linter and formatter: Ruff
3
+ - repo: https://github.com/astral-sh/ruff-pre-commit
4
+ rev: v0.6.9
5
5
  hooks:
6
- - id: black
7
- name: black
8
- language_version: python3
9
- args: ["--line-length", "88"]
6
+ # Lint + autofix. If Ruff fixes something, the hook exits nonzero to force a re-run.
7
+ - id: ruff
8
+ name: ruff (lint + fix)
9
+ args: ["--fix", "--exit-non-zero-on-fix"]
10
+ # Code formatter (Black replacement)
11
+ - id: ruff-format
12
+ name: ruff (format)
10
13
 
11
14
  # Local hooks that run in sequence and do not receive file args
12
15
  - repo: local
@@ -28,10 +31,9 @@ repos:
28
31
  always_run: true
29
32
 
30
33
  # 3) Python tests under coverage
31
- # This both runs pytest and generates .coverage data
32
34
  - id: pytest
33
35
  name: pytest (under coverage)
34
36
  entry: pytest
35
37
  language: system
36
38
  pass_filenames: false
37
- always_run: true
39
+ always_run: true
@@ -22,7 +22,7 @@ checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
22
22
 
23
23
  [[package]]
24
24
  name = "fastquadtree"
25
- version = "0.5.0"
25
+ version = "0.5.1"
26
26
  dependencies = [
27
27
  "pyo3",
28
28
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "fastquadtree"
3
- version = "0.5.0"
3
+ version = "0.5.1"
4
4
  edition = "2021"
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastquadtree
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3 :: Only
6
6
  Classifier: Programming Language :: Rust
@@ -11,6 +11,13 @@ Classifier: Topic :: Scientific/Engineering :: Information Analysis
11
11
  Classifier: Topic :: Software Development :: Libraries
12
12
  Classifier: Typing :: Typed
13
13
  Classifier: License :: OSI Approved :: MIT License
14
+ Requires-Dist: ruff>=0.6.0 ; extra == 'dev'
15
+ Requires-Dist: pytest>=7.0 ; extra == 'dev'
16
+ Requires-Dist: pytest-cov>=4.1 ; extra == 'dev'
17
+ Requires-Dist: coverage>=7.5 ; extra == 'dev'
18
+ Requires-Dist: mypy>=1.10 ; extra == 'dev'
19
+ Requires-Dist: build>=1.2.1 ; extra == 'dev'
20
+ Provides-Extra: dev
14
21
  License-File: LICENSE
15
22
  Summary: Rust-accelerated quadtree for Python with fast inserts, range queries, and k-NN search.
16
23
  Keywords: quadtree,spatial-index,geometry,rust,pyo3,nearest-neighbor,k-nn
@@ -36,7 +43,8 @@ Project-URL: Issues, https://github.com/Elan456/fastquadtree/issues
36
43
 
37
44
  [![Rust core via PyO3](https://img.shields.io/badge/Rust-core%20via%20PyO3-orange)](https://pyo3.rs/)
38
45
  [![Built with maturin](https://img.shields.io/badge/Built%20with-maturin-1f6feb)](https://www.maturin.rs/)
39
- [![Code style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
46
+ [![Lint and format: Ruff](https://img.shields.io/badge/Lint%20and%20format-Ruff-46a758?logo=ruff&logoColor=white)](https://docs.astral.sh/ruff/)
47
+
40
48
 
41
49
 
42
50
  ![Interactive_V2_Screenshot](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/interactive_v2_screenshot.png)
@@ -13,7 +13,8 @@
13
13
 
14
14
  [![Rust core via PyO3](https://img.shields.io/badge/Rust-core%20via%20PyO3-orange)](https://pyo3.rs/)
15
15
  [![Built with maturin](https://img.shields.io/badge/Built%20with-maturin-1f6feb)](https://www.maturin.rs/)
16
- [![Code style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
16
+ [![Lint and format: Ruff](https://img.shields.io/badge/Lint%20and%20format-Ruff-46a758?logo=ruff&logoColor=white)](https://docs.astral.sh/ruff/)
17
+
17
18
 
18
19
 
19
20
  ![Interactive_V2_Screenshot](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/interactive_v2_screenshot.png)
@@ -6,11 +6,11 @@ import gc
6
6
  import random
7
7
  import statistics as stats
8
8
  from time import perf_counter as now
9
+
9
10
  from tqdm import tqdm
10
11
 
11
- from fastquadtree._native import QuadTree as NativeQuadTree
12
12
  from fastquadtree import QuadTree as ShimQuadTree
13
-
13
+ from fastquadtree._native import QuadTree as NativeQuadTree
14
14
 
15
15
  BOUNDS = (0.0, 0.0, 1000.0, 1000.0)
16
16
  CAPACITY = 20
@@ -68,9 +68,10 @@ def bench_shim(points, queries, *, track_objects: bool, with_objs: bool):
68
68
  return t_build, t_query
69
69
 
70
70
 
71
- def median_times(fn, points, queries, repeats: int):
71
+ def median_times(fn, points, queries, repeats: int, desc: str = "Running"):
72
+ """Run benchmark multiple times and return median times."""
72
73
  builds, queries_t = [], []
73
- for _ in tqdm(range(repeats)):
74
+ for _ in tqdm(range(repeats), desc=desc, unit="run"):
74
75
  gc.disable()
75
76
  b, q = fn(points, queries)
76
77
  gc.enable()
@@ -86,29 +87,47 @@ def main():
86
87
  ap.add_argument("--repeats", type=int, default=5)
87
88
  args = ap.parse_args()
88
89
 
90
+ print("Native vs Shim Benchmark")
91
+ print("=" * 50)
92
+ print("Configuration:")
93
+ print(f" Points: {args.points:,}")
94
+ print(f" Queries: {args.queries}")
95
+ print(f" Repeats: {args.repeats}")
96
+ print()
97
+
89
98
  rng = random.Random(SEED)
90
99
  points = gen_points(args.points, rng)
91
100
  queries = gen_queries(args.queries, rng)
92
101
 
93
102
  # Warmup to load modules
103
+ print("Warming up...")
94
104
  _ = bench_native(points[:1000], queries[:50])
95
105
  _ = bench_shim(points[:1000], queries[:50], track_objects=False, with_objs=False)
106
+ print()
96
107
 
108
+ print("Running benchmarks...")
97
109
  n_build, n_query = median_times(
98
- lambda pts, qs: bench_native(pts, qs), points, queries, args.repeats
110
+ lambda pts, qs: bench_native(pts, qs),
111
+ points,
112
+ queries,
113
+ args.repeats,
114
+ desc="Native",
99
115
  )
100
116
  s_build_no_map, s_query_no_map = median_times(
101
117
  lambda pts, qs: bench_shim(pts, qs, track_objects=False, with_objs=False),
102
118
  points,
103
119
  queries,
104
120
  args.repeats,
121
+ desc="Shim (no map)",
105
122
  )
106
123
  s_build_map, s_query_map = median_times(
107
124
  lambda pts, qs: bench_shim(pts, qs, track_objects=True, with_objs=True),
108
125
  points,
109
126
  queries,
110
127
  args.repeats,
128
+ desc="Shim (track+objs)",
111
129
  )
130
+ print()
112
131
 
113
132
  def fmt(x):
114
133
  return f"{x:.3f}"
@@ -131,7 +150,7 @@ def main():
131
150
 
132
151
  **Overhead vs Native**
133
152
 
134
- - No map: build {s_build_no_map / n_build:.2f}x, query {s_query_no_map / n_query:.2f}x, total {(s_build_no_map + s_query_no_map) / (n_build + n_query):.2f}x
153
+ - No map: build {s_build_no_map / n_build:.2f}x, query {s_query_no_map / n_query:.2f}x, total {(s_build_no_map + s_query_no_map) / (n_build + n_query):.2f}x
135
154
  - Track + objs: build {s_build_map / n_build:.2f}x, query {s_query_map / n_query:.2f}x, total {(s_build_map + s_query_map) / (n_build + n_query):.2f}x
136
155
  """
137
156
  print(md.strip())
@@ -6,8 +6,8 @@ implementations, including performance comparison, visualization, and analysis.
6
6
  """
7
7
 
8
8
  from .engines import Engine, get_engines
9
- from .runner import BenchmarkRunner, BenchmarkConfig
10
9
  from .plotting import PlotManager
10
+ from .runner import BenchmarkConfig, BenchmarkRunner
11
11
 
12
12
  __version__ = "1.0.0"
13
- __all__ = ["Engine", "get_engines", "BenchmarkRunner", "BenchmarkConfig", "PlotManager"]
13
+ __all__ = ["BenchmarkConfig", "BenchmarkRunner", "Engine", "PlotManager", "get_engines"]
@@ -7,9 +7,11 @@ allowing fair comparison of their performance characteristics.
7
7
 
8
8
  from typing import Any, Callable, Dict, List, Optional, Tuple
9
9
 
10
+ from pyqtree import Index as PyQTree # Pyqtree
11
+
10
12
  # Built-in engines (always available in this repo)
11
13
  from pyquadtree.quadtree import QuadTree as EPyQuadTree # e-pyquadtree
12
- from pyqtree import Index as PyQTree # Pyqtree
14
+
13
15
  from fastquadtree import QuadTree as RustQuadTree # fastquadtree
14
16
 
15
17
 
@@ -67,7 +69,10 @@ def _create_e_pyquadtree_engine(
67
69
  _ = qt.query(q)
68
70
 
69
71
  return Engine(
70
- "e-pyquadtree", "#1f77b4", build, query # display name # color (blue)
72
+ "e-pyquadtree",
73
+ "#1f77b4",
74
+ build,
75
+ query, # display name # color (blue)
71
76
  )
72
77
 
73
78
 
@@ -105,7 +110,10 @@ def _create_fastquadtree_engine(
105
110
  _ = qt.query(q)
106
111
 
107
112
  return Engine(
108
- "fastquadtree", "#ff7f0e", build, query # display name # color (orange)
113
+ "fastquadtree",
114
+ "#ff7f0e",
115
+ build,
116
+ query, # display name # color (orange)
109
117
  )
110
118
 
111
119
 
@@ -165,7 +173,10 @@ def _create_nontree_engine(
165
173
  _ = tm.get_rect((xmin, ymin, xmax - xmin, ymax - ymin))
166
174
 
167
175
  return Engine(
168
- "nontree-QuadTree", "#17becf", build, query # display name # color (cyan)
176
+ "nontree-QuadTree",
177
+ "#17becf",
178
+ build,
179
+ query, # display name # color (cyan)
169
180
  )
170
181
 
171
182
 
@@ -178,7 +189,7 @@ def _create_brute_force_engine(
178
189
  # Append each item as if they were being added separately
179
190
  out = []
180
191
  for p in points:
181
- out.append(p)
192
+ out.append(p) # noqa: PERF402
182
193
  return out
183
194
 
184
195
  def query(points, queries):
@@ -187,7 +198,10 @@ def _create_brute_force_engine(
187
198
  _ = [p for p in points if q[0] <= p[0] <= q[2] and q[1] <= p[1] <= q[3]]
188
199
 
189
200
  return Engine(
190
- "Brute force", "#9467bd", build, query # display name # color (purple)
201
+ "Brute force",
202
+ "#9467bd",
203
+ build,
204
+ query, # display name # color (purple)
191
205
  )
192
206
 
193
207
 
@@ -213,8 +227,7 @@ def _create_rtree_engine(
213
227
  # Bulk stream loading is the fastest way to build
214
228
  # Keep the same 1x1 bbox convention used elsewhere for fairness
215
229
  stream = ((i, (x, y, x + 1, y + 1), None) for i, (x, y) in enumerate(points))
216
- idx = rindex.Index(stream, properties=p)
217
- return idx
230
+ return rindex.Index(stream, properties=p)
218
231
 
219
232
  def query(idx, queries):
220
233
  # Do not materialize results into a list, just consume the generator
@@ -10,8 +10,8 @@ import argparse
10
10
  from pathlib import Path
11
11
 
12
12
  from .engines import get_engines
13
- from .runner import BenchmarkRunner, BenchmarkConfig
14
13
  from .plotting import PlotManager
14
+ from .runner import BenchmarkConfig, BenchmarkRunner
15
15
 
16
16
 
17
17
  def main():
@@ -5,7 +5,7 @@ This module handles creation of performance charts, graphs, and visualizations
5
5
  for benchmark results analysis.
6
6
  """
7
7
 
8
- from typing import Dict, Any, Tuple
8
+ from typing import Any, Dict, Tuple
9
9
 
10
10
  import plotly.graph_objects as go
11
11
  from plotly.subplots import make_subplots
@@ -43,7 +43,7 @@ class PlotManager:
43
43
  name=name,
44
44
  legendgroup=name,
45
45
  showlegend=show_legend,
46
- line=dict(color=color, width=3),
46
+ line={"color": color, "width": 3},
47
47
  ),
48
48
  row=1,
49
49
  col=col,
@@ -69,15 +69,15 @@ class PlotManager:
69
69
  f"{self.config.n_queries} queries)"
70
70
  ),
71
71
  template="plotly_dark",
72
- legend=dict(
73
- orientation="v",
74
- traceorder="normal",
75
- xanchor="left",
76
- x=0,
77
- yanchor="top",
78
- y=1,
79
- ),
80
- margin=dict(l=40, r=20, t=80, b=40),
72
+ legend={
73
+ "orientation": "v",
74
+ "traceorder": "normal",
75
+ "xanchor": "left",
76
+ "x": 0,
77
+ "yanchor": "top",
78
+ "y": 1,
79
+ },
80
+ margin={"l": 40, "r": 20, "t": 80, "b": 40},
81
81
  height=520,
82
82
  )
83
83
 
@@ -108,7 +108,7 @@ class PlotManager:
108
108
  name=name,
109
109
  legendgroup=name,
110
110
  showlegend=False,
111
- line=dict(color=color, width=3),
111
+ line={"color": color, "width": 3},
112
112
  ),
113
113
  row=1,
114
114
  col=1,
@@ -122,7 +122,7 @@ class PlotManager:
122
122
  name=name,
123
123
  legendgroup=name,
124
124
  showlegend=True,
125
- line=dict(color=color, width=3),
125
+ line={"color": color, "width": 3},
126
126
  ),
127
127
  row=1,
128
128
  col=2,
@@ -138,14 +138,14 @@ class PlotManager:
138
138
  fig.update_layout(
139
139
  title="Throughput",
140
140
  template="plotly_dark",
141
- legend=dict(
142
- orientation="h",
143
- x=0,
144
- xanchor="left",
145
- y=1.08, # above the subplots
146
- yanchor="bottom",
147
- ),
148
- margin=dict(l=60, r=40, t=120, b=40),
141
+ legend={
142
+ "orientation": "h",
143
+ "x": 0,
144
+ "xanchor": "left",
145
+ "y": 1.08, # above the subplots
146
+ "yanchor": "bottom",
147
+ },
148
+ margin={"l": 60, "r": 40, "t": 120, "b": 40},
149
149
  height=480,
150
150
  )
151
151
 
@@ -187,7 +187,7 @@ class PlotManager:
187
187
  height=480,
188
188
  )
189
189
  print(f"Saved PNG images to {output_dir}/ with prefix '{output_prefix}'")
190
- except Exception as e:
190
+ except Exception as e: # noqa: BLE001
191
191
  print(
192
192
  f"Failed to save PNG images. Install kaleido to enable PNG export: {e}"
193
193
  )
@@ -227,7 +227,7 @@ class PlotManager:
227
227
  x=config.experiments,
228
228
  y=values,
229
229
  name=f"{label} - {engine_name}",
230
- line=dict(color=color, width=3),
230
+ line={"color": color, "width": 3},
231
231
  mode="lines+markers",
232
232
  )
233
233
  )
@@ -237,7 +237,7 @@ class PlotManager:
237
237
  xaxis_title="Number of points",
238
238
  yaxis_title="Time (s)" if "rate" not in metric else "Ops/sec",
239
239
  template="plotly_dark",
240
- legend=dict(orientation="v"),
240
+ legend={"orientation": "v"},
241
241
  height=600,
242
242
  )
243
243
 
@@ -11,7 +11,7 @@ import random
11
11
  import statistics as stats
12
12
  from dataclasses import dataclass
13
13
  from time import perf_counter as now
14
- from typing import Dict, List, Tuple, Any
14
+ from typing import Any, Dict, List, Tuple
15
15
 
16
16
  from tqdm import tqdm
17
17
 
@@ -35,8 +35,7 @@ class BenchmarkConfig:
35
35
  self.experiments = [2, 4, 8, 16]
36
36
  while self.experiments[-1] < self.max_experiment_points:
37
37
  self.experiments.append(int(self.experiments[-1] * 2))
38
- if self.experiments[-1] > self.max_experiment_points:
39
- self.experiments[-1] = self.max_experiment_points
38
+ self.experiments[-1] = min(self.experiments[-1], self.max_experiment_points)
40
39
 
41
40
 
42
41
  class BenchmarkRunner:
@@ -98,6 +97,44 @@ class BenchmarkRunner:
98
97
  cleaned = [x for x in vals if isinstance(x, (int, float)) and not math.isnan(x)]
99
98
  return stats.median(cleaned) if cleaned else math.nan
100
99
 
100
+ def _print_experiment_summary(
101
+ self, n: int, results: Dict[str, Any], exp_idx: int
102
+ ) -> None:
103
+ """Print a summary of results for the current experiment."""
104
+
105
+ def fmt(x):
106
+ return f"{x:.3f}" if not math.isnan(x) else "nan"
107
+
108
+ # Get the results for this experiment (last index)
109
+ total = results["total"]
110
+ build = results["build"]
111
+ query = results["query"]
112
+
113
+ # Find the fastest engine for this experiment
114
+ valid_engines = [
115
+ (name, total[name][exp_idx])
116
+ for name in total
117
+ if not math.isnan(total[name][exp_idx])
118
+ ]
119
+
120
+ if not valid_engines:
121
+ return
122
+
123
+ fastest = min(valid_engines, key=lambda x: x[1])
124
+
125
+ print(f"\n 📊 Results for {n:,} points:")
126
+ print(f" Fastest: {fastest[0]} ({fmt(fastest[1])}s total)")
127
+
128
+ # Show top 3 performers
129
+ sorted_engines = sorted(valid_engines, key=lambda x: x[1])[:3]
130
+ for rank, (name, time) in enumerate(sorted_engines, 1):
131
+ b = build[name][exp_idx]
132
+ q = query[name][exp_idx]
133
+ print(
134
+ f" {rank}. {name:15} build={fmt(b)}s, query={fmt(q)}s, total={fmt(time)}s"
135
+ )
136
+ print()
137
+
101
138
  def run_benchmark(self, engines: Dict[str, Engine]) -> Dict[str, Any]:
102
139
  """
103
140
  Run complete benchmark suite.
@@ -109,13 +146,11 @@ class BenchmarkRunner:
109
146
  Dictionary containing benchmark results
110
147
  """
111
148
  # Warmup on a small set to JIT caches, etc.
149
+ print("Warming up engines...")
112
150
  warmup_points = self.generate_points(2_000)
113
151
  warmup_queries = self.generate_queries(self.config.n_queries)
114
152
  for engine in engines.values():
115
- try:
116
- self.benchmark_engine_once(engine, warmup_points, warmup_queries)
117
- except Exception:
118
- pass # Ignore warmup failures
153
+ self.benchmark_engine_once(engine, warmup_points, warmup_queries)
119
154
 
120
155
  # Initialize result containers
121
156
  results = {
@@ -127,9 +162,18 @@ class BenchmarkRunner:
127
162
  }
128
163
 
129
164
  # Run experiments
130
- iterator = tqdm(self.config.experiments, desc="Experiments", unit="points")
131
- for n in iterator:
132
- iterator.set_postfix({"points": n})
165
+ print(
166
+ f"\nRunning {len(self.config.experiments)} experiments with {len(engines)} engines..."
167
+ )
168
+ experiment_bar = tqdm(
169
+ self.config.experiments, desc="Experiments", unit="exp", position=0
170
+ )
171
+
172
+ for exp_idx, n in enumerate(experiment_bar):
173
+ experiment_bar.set_description(
174
+ f"Experiment {exp_idx + 1}/{len(self.config.experiments)}"
175
+ )
176
+ experiment_bar.set_postfix({"points": f"{n:,}"})
133
177
 
134
178
  # Generate data for this experiment
135
179
  exp_rng = random.Random(10_000 + n)
@@ -139,24 +183,45 @@ class BenchmarkRunner:
139
183
  # Collect results across repeats
140
184
  engine_times = {name: {"build": [], "query": []} for name in engines}
141
185
 
186
+ # Progress bar for engines x repeats
187
+ total_iterations = len(engines) * self.config.repeats
188
+ engine_bar = tqdm(
189
+ total=total_iterations,
190
+ desc=" Testing engines",
191
+ unit="run",
192
+ position=1,
193
+ leave=False,
194
+ )
195
+
142
196
  for repeat in range(self.config.repeats):
143
197
  gc.disable()
144
198
 
145
199
  # Benchmark each engine
146
200
  for name, engine in engines.items():
201
+ engine_bar.set_description(
202
+ f" {name} (repeat {repeat + 1}/{self.config.repeats})"
203
+ )
204
+
147
205
  try:
148
206
  build_time, query_time = self.benchmark_engine_once(
149
207
  engine, points, queries
150
208
  )
151
- except Exception:
209
+ except Exception as e: # noqa: BLE001
152
210
  # Mark as failed for this repeat
211
+ print(
212
+ f" {name} (repeat {repeat + 1}/{self.config.repeats}) failed: {e}"
213
+ )
153
214
  build_time, query_time = math.nan, math.nan
154
215
 
155
216
  engine_times[name]["build"].append(build_time)
156
217
  engine_times[name]["query"].append(query_time)
157
218
 
219
+ engine_bar.update(1)
220
+
158
221
  gc.enable()
159
222
 
223
+ engine_bar.close()
224
+
160
225
  # Calculate medians and derived metrics
161
226
  for name in engines:
162
227
  build_median = self.median_or_nan(engine_times[name]["build"])
@@ -184,6 +249,11 @@ class BenchmarkRunner:
184
249
  results["insert_rate"][name].append(insert_rate)
185
250
  results["query_rate"][name].append(query_rate)
186
251
 
252
+ # Print intermediate results for this experiment
253
+ self._print_experiment_summary(n, results, exp_idx)
254
+
255
+ experiment_bar.close()
256
+
187
257
  # Add metadata to results
188
258
  results["engines"] = engines
189
259
  results["config"] = self.config
@@ -227,7 +297,7 @@ class BenchmarkRunner:
227
297
  t = total[name][i]
228
298
  if math.isnan(pyqt_total) or math.isnan(t) or t <= 0:
229
299
  return "n/a"
230
- return f"{(pyqt_total / t):.2f}×"
300
+ return f"{(pyqt_total / t):.2f}×" # noqa: RUF001
231
301
 
232
302
  for name in ranked:
233
303
  b = build.get(name, [math.nan])[i] if name in build else math.nan
@@ -6,14 +6,7 @@ This script can be run directly or imported as a module.
6
6
  """
7
7
 
8
8
  import sys
9
- from pathlib import Path
10
9
 
11
- # Add the benchmarks directory to Python path for imports
12
- benchmark_dir = Path(__file__).parent
13
- if str(benchmark_dir) not in sys.path:
14
- sys.path.insert(0, str(benchmark_dir))
15
-
16
- # Now we can import the package
17
10
  from quadtree_bench.main import main, run_quick_benchmark
18
11
 
19
12
  if __name__ == "__main__":
@@ -1,6 +1,9 @@
1
- from fastquadtree import QuadTree
2
- import pygame
3
1
  import random
2
+ import sys
3
+
4
+ import pygame
5
+
6
+ from fastquadtree import QuadTree
4
7
 
5
8
  screen = pygame.display.set_mode((1000, 1000))
6
9
 
@@ -81,7 +84,7 @@ def interactive_test():
81
84
  event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE
82
85
  ):
83
86
  pygame.quit()
84
- quit()
87
+ sys.exit()
85
88
 
86
89
  if event.type == pygame.MOUSEBUTTONDOWN:
87
90
  x, y = pygame.mouse.get_pos()
@@ -90,9 +93,12 @@ def interactive_test():
90
93
  qtree.insert((x, y), obj=Ball(x, y))
91
94
  # print("New total number of nodes: ", len(qtree.get_all_bbox()))
92
95
 
93
- if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
94
- if closest_to_mouse:
95
- qtree.delete_by_object(closest_to_mouse.obj)
96
+ if (
97
+ event.type == pygame.KEYDOWN
98
+ and event.key == pygame.K_SPACE
99
+ and closest_to_mouse
100
+ ):
101
+ qtree.delete_by_object(closest_to_mouse.obj)
96
102
 
97
103
  # If right arrow down, move the query area to the right
98
104
  if pygame.key.get_pressed()[pygame.K_RIGHT]: