Skip to content

Commit 1a465ed

Browse files
authored
Merge pull request #28 from tlambert03/bench
Add benchmark tests on CI
2 parents 8e23297 + 3325dc3 commit 1a465ed

5 files changed

Lines changed: 107 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,25 @@ jobs:
4242
with:
4343
token: ${{ secrets.CODECOV_TOKEN }}
4444

45+
benchmarks:
46+
runs-on: ubuntu-latest
47+
env:
48+
UV_NO_SYNC: "1"
49+
steps:
50+
- uses: actions/checkout@v4
51+
- uses: astral-sh/setup-uv@v6
52+
with:
53+
python-version: "3.13"
54+
enable-cache: true
55+
56+
- name: install
57+
run: uv sync --no-dev --group test-codspeed
58+
59+
- name: Run benchmarks
60+
uses: CodSpeedHQ/action@v3
61+
with:
62+
run: uv run pytest -W ignore --codspeed -v --color=yes
63+
4564
deploy:
4665
name: Deploy
4766
needs: test

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ repos:
1717
- repo: https://github.com/astral-sh/ruff-pre-commit
1818
rev: v0.12.2
1919
hooks:
20-
- id: ruff
20+
- id: ruff-check
2121
args: [--fix, --unsafe-fixes]
2222
- id: ruff-format
2323

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
[![Python Version](https://img.shields.io/pypi/pyversions/spatial-graph.svg?color=green)](https://python.org)
66
[![CI](https://github.com/funkelab/spatial_graph/actions/workflows/ci.yaml/badge.svg)](https://github.com/funkelab/spatial_graph/actions/workflows/ci.yaml)
77
[![codecov](https://codecov.io/gh/funkelab/spatial_graph/branch/main/graph/badge.svg)](https://codecov.io/gh/funkelab/spatial_graph)
8+
[![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/funkelab/spatial_graph)
89

910
`spatial_graph` provides a data structure for directed and undirected graphs,
1011
where each node has an nD position (in time or space).

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ dependencies = ["witty>=v0.2.1", "CT3>=3.3.3", "numpy", "setuptools>=75.8.0"]
3131

3232
[dependency-groups]
3333
test = ["pytest>=8.3.5", "pytest-cov>=6.1.1"]
34+
test-codspeed = [{ include-group = "test" }, "pytest-codspeed >=3.2.0"]
3435
dev = [
3536
{ include-group = "test" },
3637
"ipython>=8.18.1",
3738
"mypy>=1.15.0",
3839
"pre-commit>=4.2.0",
40+
"pytest-benchmark>=5.1.0", # specifically excluded from test group for ci
3941
"ruff>=0.11.10",
4042
]
4143
docs = [

tests/test_bench.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import sys
2+
3+
import numpy as np
4+
import pytest
5+
6+
from spatial_graph import SpatialGraph
7+
8+
# either run this file directly or with pytest --codspeed
9+
if all(x not in {"--codspeed", "tests/test_bench.py"} for x in sys.argv):
10+
pytest.skip(
11+
"use 'pytest tests/test_bench.py' to run benchmark", allow_module_level=True
12+
)
13+
14+
15+
def _make_graph(
16+
ndims=3,
17+
node_dtype="uint64",
18+
node_attr_dtypes=None,
19+
edge_attr_dtypes=None,
20+
directed=False,
21+
n_nodes=100_000,
22+
):
23+
"""Helper to create a SpatialGraph instance with default parameters."""
24+
if node_attr_dtypes is None:
25+
node_attr_dtypes = {"position": "double[3]"}
26+
if edge_attr_dtypes is None:
27+
edge_attr_dtypes = {"score": "float32"}
28+
29+
graph = SpatialGraph(
30+
ndims=ndims,
31+
node_dtype=node_dtype,
32+
node_attr_dtypes=node_attr_dtypes,
33+
edge_attr_dtypes=edge_attr_dtypes,
34+
position_attr="position",
35+
directed=directed,
36+
)
37+
nodes = np.arange(n_nodes, dtype="uint64")
38+
positions = np.random.random((n_nodes, ndims))
39+
graph.add_nodes(nodes, position=positions)
40+
41+
return graph
42+
43+
44+
@pytest.mark.parametrize("num_queries", [100])
45+
@pytest.mark.parametrize("k", [1000, 10000])
46+
@pytest.mark.parametrize("n_nodes", [100_000, 1_000_000])
47+
def test_bench_query_nearest_nodes(n_nodes: int, k: int, num_queries: int, benchmark):
48+
"""Benchmark query_nearest_nodes."""
49+
graph = _make_graph(n_nodes=n_nodes)
50+
query_points = np.random.random((num_queries, 3))
51+
52+
def _run():
53+
for i in range(num_queries):
54+
# Query nearest nodes
55+
closest, distances = graph.query_nearest_nodes(
56+
query_points[i], k=k, return_distances=True
57+
)
58+
positions = graph.node_attrs[closest].position
59+
return closest, distances, positions
60+
61+
closest, distances, positions = benchmark(_run)
62+
63+
# Verify results
64+
assert len(distances) == len(closest)
65+
assert positions.shape[1] == 3
66+
67+
68+
@pytest.mark.parametrize("n_nodes", [100_000, 1_000_000])
69+
def test_roi_query_performance(n_nodes, benchmark):
70+
"""Benchmark ROI (region of interest) queries."""
71+
large_graph = _make_graph(n_nodes=n_nodes)
72+
# Define a ROI that should contain a reasonable number of nodes
73+
roi = np.array([[0.25, 0.25, 0.25], [0.75, 0.75, 0.75]])
74+
75+
nodes_in_roi = benchmark(lambda: large_graph.query_nodes_in_roi(roi))
76+
77+
# Verify results
78+
assert len(nodes_in_roi) > 0
79+
assert len(nodes_in_roi) < n_nodes # Should be subset
80+
81+
# Verify nodes are actually in ROI
82+
positions = large_graph.node_attrs[nodes_in_roi].position
83+
assert np.all(positions >= roi[0])
84+
assert np.all(positions <= roi[1])

0 commit comments

Comments
 (0)