Skip to content

Commit 6103438

Browse files
authored
Merge pull request #64 from petercorke/docs/readme-london-benchmark
2 parents cdbc54a + d555170 commit 6103438

4 files changed

Lines changed: 118 additions & 37 deletions

File tree

‎README.md‎

Lines changed: 117 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -36,42 +36,65 @@
3636

3737
This pure-Python package provides classes for directed and undirected graphs: vertices
3838
connected by edges, with edge costs and path planning. Self-loops and
39-
parallel edges are permitted.
39+
parallel edges are permitted. Performance is good enough to support bundle adjustment, pose-graph SLAM and path planning over thousands of vertices.
4040

4141
Vertices can optionally be *embedded*: given a coordinate in a space of any
4242
dimension. Edge costs are then computed from the vertex positions using a
4343
distance metric (Euclidean, Manhattan, SE(2), or your own), the vertex nearest
4444
a point can be found, `A*` uses the metric as its heuristic, and the graph can
4545
be plotted, or exported with its layout preserved.
4646

47-
![road network](https://github.com/petercorke/pgraph-python/raw/main/examples/roads.png)
47+
## Installation
4848

4949
```
50-
from pgraph import *
50+
pip install pgraph-python
51+
```
52+
53+
This needs Python 3.9 or later. The dependencies, NumPy, Matplotlib and Spatial Maths, are installed with it.
54+
55+
## Examples
56+
57+
### A small example: routing through towns in Queensland
58+
59+
![Map of 20 towns in Queensland as blue circles joined by roads, with the shortest route from Hughenden to Brisbane, 1,659 km via Barcaldine, Blackall, Charleville and Roma, highlighted in red](https://github.com/petercorke/pgraph-python/raw/main/examples/roads.png)
60+
61+
```python
62+
from pgraph import UGraph
5163
import json
5264

53-
# load places and routes
54-
with open('places.json', 'r') as f:
55-
places = json.loads(f.read())
56-
with open('routes.json', 'r') as f:
57-
routes = json.loads(f.read())
65+
# load the places and routes
66+
with open('queensland.json', 'r') as f:
67+
data = json.load(f)
5868

5969
# build the graph
6070
g = UGraph()
61-
62-
for name, info in places.items():
63-
g.add_vertex(name=name, coord=info["utm"])
64-
65-
for route in routes:
66-
g.add_edge(route[0], route[1], cost=route[2])
71+
for name, info in data['places'].items():
72+
g.add_vertex(name=name, coord=info['utm'])
73+
for route in data['routes']:
74+
g.add_edge(route['start'], route['end'], cost=route['distance'])
6775

6876
# plan a path from Hughenden to Brisbane
6977
p, length, _ = g.path_Astar('Hughenden', 'Brisbane')
70-
g.plot(block=False) # plot it
78+
g.plot(block=False) # plot it
7179
g.highlight_path(p) # overlay the path
7280
```
7381

82+
The data file and the complete example are in the notebook [`examples/roads.ipynb`](https://github.com/petercorke/pgraph-python/blob/main/examples/roads.ipynb).
83+
84+
### A bigger example: routing through central London
85+
86+
![Map of central London as a network of grey streets, with two routes between Buckingham Palace and Tower Bridge: a solid blue route of 5,515 m via Big Ben, and a dashed orange route of 5,948 m back through the north of the area, with five landmarks labelled](https://github.com/petercorke/pgraph-python/raw/main/docs/figs/london_route.png)
87+
88+
A directed graph of the road network of central London, taken from [OpenStreetMap](https://www.openstreetmap.org/copyright), and the shortest drive between Buckingham Palace and Tower Bridge, there (solid) and back (dashed). The two routes differ in length, 5,515 m and 5,948 m, because of the one-way streets. The notebook [`examples/london.ipynb`](https://github.com/petercorke/pgraph-python/blob/main/examples/london.ipynb) builds the graph from the map data, keeps the part of it that can be driven around in, and plans with `A*`. This graph has over 4,000 vertices.
89+
90+
Map data © OpenStreetMap contributors, available under the [Open Database License](https://www.openstreetmap.org/copyright).
91+
92+
## Using pgraph
93+
94+
Full details are given in the [online documentation](https://petercorke.github.io/pgraph-python).
95+
7496
### Properties and methods of the graph
97+
7598
Graphs belong to the class `UGraph` or `DGraph` for undirected or directed graphs respectively. The graph is essentially a container for the vertices.
7699

77100
- `g.add_vertex()` add a vertex
@@ -84,23 +107,39 @@ Graphs belong to the class `UGraph` or `DGraph` for undirected or directed graph
84107
- `g.edges()` all edges in the graph
85108
- `g.plot()` plots the vertices and edges
86109
- `g.nc` the number of graph components, 1 if fully connected
87-
- `g.component(v)` the component that vertex `v` belongs to
110+
- `g.component(c)` the vertices in component number `c`, numbered from 0
88111
- `g.samecomponent(v1, v2)` predicate for vertices belonging to the same component
89112

90113
***
91-
- `g.path_BFS()` breadth-first search
92-
- `g.path_Astar()` A* search
114+
- `g.path_BFS()` breadth-first search, the path with the fewest edges
115+
- `g.path_UCS()` uniform-cost search, the cheapest path
116+
- `g.path_Astar()` A* search, the cheapest path using a heuristic
117+
118+
***
119+
- `g.closest(point)` the vertex nearest to a point
120+
- `g.near(point, radius)` the vertices within a distance of a point
121+
- `g.within(box)` the vertices inside a box
122+
- `g.subgraph(vertices)` the part of the graph that those vertices induce, as a new graph
123+
- `g.coordinates()` the vertex coordinates as a matrix, a column per vertex
124+
- `g.build_kdtree()` an optional spatial index (needs scipy) that makes `closest` and `near` faster on a large graph
125+
- `g.metric` the distance metric: `"L2"` (the default), `"L1"`, `"SE2"`, `SE2Metric(weight)` that weights the angle of a pose, or a function of your own
93126

94127
***
95128
- `g.adjacency()` adjacency matrix
96129
- `g.laplacian()` Laplacian matrix
97130
- `g.incidence()` incidence matrix
98131

132+
***
133+
- `g.copy()` a deep copy; a graph can also be pickled
134+
- `g.export()` write the graph as GraphViz DOT, Mermaid, GraphML, ELK, or an image (which needs GraphViz)
135+
- `g.validate()` check that the graph's records agree with each other
136+
99137
### Properties and methods of a vertex
100138
Vertices are instances of the class `Vertex`, for both undirected and directed graphs.
101139

102140
- `v.coord` the coordinate vector for embedded graph (optional)
103141
- `v.name` the name of the vertex (optional)
142+
- `v.label` the number of the component that the vertex belongs to
104143
- `v.adjacent()` is a list of the adjacent vertices (`neighbours()` and `neighbors()` are the same)
105144

106145
Vertices can be named and referenced by name.
@@ -113,25 +152,29 @@ Each edge references a pair of vertices, the graph lists all of its edges in `g.
113152
- `e.next(v)` vertex on edge `e` that is not `v`
114153
- `e.v1`, `e.v2` the two vertices that define the edge `e`
115154

116-
## Modifying a graph
155+
### Modifying a graph
117156

118157
- `g.remove_vertex(v)` or `v.remove()` remove vertex `v`
119158
- `g.remove_edge(e)` or `e.remove()` remove edge `e`
120159

121-
## Subclassing pgraph classes
160+
### Subclassing `pgraph` classes
122161

123162
Consider a user class `Foo` that we would like to connect using a graph _overlay_, i.e.
124163
instances of `Foo` become vertices in a graph.
125164

126165
- Have it subclass `Vertex`
127166
- Then place instances of `Foo` into the graph using `add_vertex` and create edges as required
128167

129-
```
168+
```python
169+
from pgraph import UGraph, Vertex
170+
130171
class Foo(Vertex):
131-
# foo stuff goes here
132-
133-
f1 = Foo(...)
134-
f2 = Foo(...)
172+
# foo stuff goes here
173+
def say_hi(self):
174+
print(f"hi from {self.name}")
175+
176+
f1 = Foo(name="f1")
177+
f2 = Foo(name="f2")
135178

136179
g = UGraph() # create a new undirected graph
137180
g.add_vertex(f1)
@@ -142,14 +185,52 @@ for f in f1.adjacent():
142185
f.say_hi() # say hi to the neighbours
143186
```
144187

145-
## MATLAB version
146-
147-
This is a re-engineered version of [PGraph.m](https://github.com/petercorke/spatialmath-matlab/blob/main/PGraph.m) which ships as part of the [Spatial Math Toolbox for MATLAB](https://github.com/petercorke/spatialmath-matlab). This class is used to support bundle adjustment, pose-graph SLAM and various planners such as PRM, RRT and Lattice.
148-
149-
The Python version was designed from the start to work with directed and undirected graphs, whereas directed graphs were a late addition to the MATLAB version. Semantics are similar but not identical. In particular the use of subclassing rather than references to
150-
_user data_ is encouraged.
151-
152-
153-
154-
155-
188+
Or tell the graph which classes to make, with `UGraph(vertex_class=Foo, edge_class=Bar)`, and `g.add_vertex()` and `g.add_edge()` create instances of them, so they never need to be constructed by hand.
189+
190+
## Performance
191+
192+
Timings of the key operations on random geometric graphs, each vertex joined to its 4 nearest neighbours, of 100, 1,000 and 10,000 vertices, measured with [`examples/benchmark.py`](https://github.com/petercorke/pgraph-python/blob/main/examples/benchmark.py) on an Apple M4 with Python 3.12 and NumPy 2.5. The path times are the median of 10 random start and goal pairs. The last column is the exponent *b* in time ∝ N<sup>*b*</sup> between the smallest and the largest graph for the quantity shown, so 0 is constant, 1 is linear and 2 is quadratic. The adjacency, Laplacian and incidence matrices are dense, so they are only formed up to 1,000 vertices.
193+
194+
### Undirected graphs
195+
196+
| Operation | per | N=100 | N=1,000 | N=10,000 | growth exponent |
197+
| :----------------------------------- | :---------- | -----: | ------: | -------: | --------------: |
198+
| add a vertex | µs / vertex | 1.52 | 1.65 | 1.59 | 0.0 |
199+
| add an edge (cost from the metric) | µs / edge | 1.55 | 1.58 | 1.85 | 0.0 |
200+
| copy the graph | ms | 0.823 | 8.17 | 151 | 1.1 |
201+
| pickle and unpickle the graph | ms | 0.456 | 4.76 | 113 | 1.2 |
202+
| number of components, after a change | ms | 0.0292 | 0.319 | 3.86 | 1.1 |
203+
| number of components, unchanged | µs | 0.083 | 0.083 | 0.083 | 0.0 |
204+
| remove a vertex | µs | 6.19 | 11.6 | 56.5 | 0.5 |
205+
| closest vertex to a point | µs / query | 69.9 | 708 | 7,056 | 1.0 |
206+
| adjacency matrix | ms | 0.107 | 1.46 | – | 1.1 |
207+
| Laplacian matrix | ms | 0.121 | 2.76 | – | 1.4 |
208+
| incidence matrix | ms | 0.0565 | 1.44 | – | 1.4 |
209+
| path, breadth-first | ms / query | 0.038 | 0.317 | 5.4 | 1.1 |
210+
| path, uniform cost | ms / query | 0.0822 | 0.806 | 11.7 | 1.1 |
211+
| path, A* | ms / query | 0.0498 | 0.295 | 2.48 | 0.8 |
212+
213+
### Directed graphs
214+
215+
| Operation | per | N=100 | N=1,000 | N=10,000 | growth exponent |
216+
| :----------------------------------- | :---------- | -----: | ------: | -------: | --------------: |
217+
| add a vertex | µs / vertex | 1.53 | 1.58 | 1.59 | 0.0 |
218+
| add an edge (cost from the metric) | µs / edge | 1.56 | 1.64 | 1.84 | 0.0 |
219+
| copy the graph | ms | 1.12 | 12.1 | 227 | 1.2 |
220+
| pickle and unpickle the graph | ms | 0.573 | 6.58 | 151 | 1.2 |
221+
| number of components, after a change | ms | 0.0405 | 0.485 | 5.87 | 1.1 |
222+
| number of components, unchanged | µs | 0.083 | 0.083 | 0.083 | 0.0 |
223+
| remove a vertex | µs | 8.04 | 15.7 | 70 | 0.5 |
224+
| closest vertex to a point | µs / query | 70.2 | 711 | 6,921 | 1.0 |
225+
| adjacency matrix | ms | 0.0815 | 1.21 | – | 1.2 |
226+
| Laplacian matrix | ms | 0.0928 | 2.64 | – | 1.5 |
227+
| incidence matrix | ms | 0.0871 | 2.41 | – | 1.4 |
228+
| path, breadth-first | ms / query | 0.0225 | 0.386 | 3.87 | 1.1 |
229+
| path, uniform cost | ms / query | 0.0536 | 0.936 | 8.06 | 1.1 |
230+
| path, A* | ms / query | 0.046 | 0.721 | 3.68 | 1.0 |
231+
232+
## History
233+
234+
This is a re-engineered version of [PGraph.m](https://github.com/petercorke/spatialmath-matlab/blob/main/PGraph.m) which ships as part of the [Spatial Math Toolbox for MATLAB](https://github.com/petercorke/spatialmath-matlab). That class was used to support bundle adjustment, pose-graph SLAM and various planners such as PRM, RRT and Lattice.
235+
236+
The Python version was designed from the start to work with directed and undirected graphs, whereas directed graphs were a late addition to the MATLAB version. Semantics are similar but not identical. In particular the use of subclassing rather than references to _user data_ is encouraged.

‎docs/figs/london_route.png‎

253 KB
Loading

‎docs/source/intro.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,5 +300,5 @@ Alternatively, we can make the decision at runtime by using the ``directed`` arg
300300
History
301301
--------
302302

303-
``pgaph-python`` is based on ``PGraph.m`` which is part of the
303+
``pgraph-python`` is based on ``PGraph.m`` which is part of the
304304
earlier `Spatial Math Toolbox for MATLAB <https://github.com/petercorke/spatialmath-matlab>`_.

‎examples/roads.png‎

18.4 KB
Loading

0 commit comments

Comments
 (0)