Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a966001
Release 1.0.0
petercorke Sep 3, 2026
dc6e837
changelog: also credit MVTB as a real dependency, not just RTB
petercorke Sep 3, 2026
bf084bb
Merge remote-tracking branch 'origin/main' into chore/release-1.0.0
petercorke Oct 12, 2026
5bfbb8b
changelog: write the 1.0.0 entry for what has landed
petercorke Oct 12, 2026
b12b688
changelog: say exactly what was wrong with incidence()
petercorke Oct 12, 2026
fcfdfde
Merge remote-tracking branch 'origin/main' into chore/release-1.0.0
petercorke Oct 12, 2026
26070bf
changelog: plot(), the components hang and the label
petercorke Oct 12, 2026
218e91b
Merge remote-tracking branch 'origin/main' into chore/release-1.0.0
petercorke Oct 13, 2026
989fbfd
changelog: SE2Metric, the spatial queries, the speed-ups, the examples
petercorke Oct 13, 2026
d552e5b
ci: skip-existing, the kebab-case name of the input
petercorke Oct 13, 2026
4b325b4
changelog: speed-ups measured against 0.6.5, and the cost of add_vertex
petercorke Oct 13, 2026
272fbbf
Merge remote-tracking branch 'origin/main' into chore/release-1.0.0
petercorke Oct 13, 2026
f75f320
changelog: creating a vertex is faster than 0.6.5
petercorke Oct 13, 2026
0a8663a
Merge branch 'main' into chore/release-1.0.0
petercorke Oct 13, 2026
7f8df53
Merge remote-tracking branch 'origin/main' into chore/release-1.0.0
petercorke Oct 13, 2026
e1ff1f7
changelog: llms.txt and AGENTS.md
petercorke Oct 13, 2026
3001c37
Merge remote-tracking branch 'origin/main' into chore/release-1.0.0
petercorke Oct 13, 2026
ccf3934
changelog: the documentation site is deployed again
petercorke Oct 13, 2026
9529d8d
Merge remote-tracking branch 'origin/chore/release-1.0.0' into chore/…
petercorke Oct 13, 2026
01a1f8e
Merge remote-tracking branch 'origin/main' into chore/release-1.0.0
petercorke Oct 13, 2026
7adb665
changelog: import pgraph is 0.04 s, highlighting in 3D
petercorke Oct 13, 2026
bc647b9
Remove RELEASE
petercorke Oct 13, 2026
f06a1cc
Remove build/lib, a stale copy of the source
petercorke Oct 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,4 @@ jobs:
# pinned to a commit, as this action can publish to PyPI; dependabot updates it
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
skip_existing: true
skip-existing: true
200 changes: 200 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,203 @@
1.0.0

First 1.0 release: six years of production use since the first commit in 2020
(including as a real dependency of both the Robotics Toolbox for Python and the
Machine Vision Toolbox for Python), and this cycle's type hints, documentation,
testing and bug-fixing pass, felt like the right point to commit to API
stability. This is the first release with a considered API, so it contains
several deliberate breaking changes, listed first with how to migrate.

Breaking changes (from 0.6.5)

- The `UGraph`/`DGraph` constructor arguments are keyword-only. 0.6.5 accepted
and silently ignored a first positional argument, so `UGraph(6)` looked as
though it set a dimension but did nothing. Now it is a `TypeError`: write
`UGraph(dim=6)`.
- `degree` of a `DGraph` is the graph-theory degree, in plus out, so a
self-loop counts 2 and the degrees sum to twice the number of edges. It used
to count only the edges leaving the vertex. This changes `vertex.degree`,
`graph.degree()` and `graph.connectivity()`, and `graph.average_degree()` is
now 2E/N for both kinds of graph (it was E/N for a `DGraph`). The previous
values are `vertex.out_degree`, `[v.out_degree for v in g]` and `g.ne / g.n`.
`laplacian()` is unchanged (out-degree minus adjacency). Undirected graphs
are unchanged.
- `graph.edges()` is a `list` in the order the edges were added. It was the
graph's own `set`, with no defined order, although documented as a list. Both
`graph.edges()` and `vertex.edges()` return a new list, so changing the result
cannot corrupt the graph. Use `set(g.edges())` for set operations.
- `Edge.v1` and `Edge.v2` are read-only, and so is `Vertex.label` (the graph
assigns it, and reading it now finds the components, where 0.6.5 gave `None`
until `nc` had been asked for). Assigning an endpoint of an edge that was in a graph left the
graph inconsistent and the edge impossible to remove. Give the vertices to
`Edge(v1, v2)` or connect the edge with `v1.connect(v2, edge=e)`.
- Assigning `Vertex.name` renames it in the graph (so `g[new]` works) and
refuses a name already in use or `None`.
- `add_vertex` raises `ValueError` for a name already used in the graph or a
vertex that already belongs to a graph, instead of silently shadowing it.
The default name `#N` is now the first free one, which could previously
repeat after a removal.
- There is one vertex class, `Vertex`: a vertex has no direction of its own, the
graph it belongs to decides how an edge is recorded at its ends. `UVertex`
and `DVertex` are deprecated names for it (below). A vertex given to
`add_vertex` is no longer checked against the kind of graph, because there is
no kind to mismatch. An override of the private `_attach`/`_detach` hooks
moves from a vertex subclass to a `UGraph`/`DGraph` subclass.
- The abstract base class `PGraph` is now `Graph`, and `Graph` and `Vertex` are
exported from the package. `Graph` is concrete: `Graph(directed=True)` is a
`DGraph` and `Graph()` a `UGraph`.
- `Adjacency()` treats a non-zero diagonal element as a self-loop.
- The `str` of an edge between vertices that are not in a graph shows `--`,
because there is no kind of graph to say whether it is directed.

Deprecated (still work, with a `DeprecationWarning`)

- `Laplacian()` is `laplacian()`: a capital initial denotes a class method that
builds a graph, like `Adjacency()` and `Dict()`.
- `UVertex` and `DVertex` are `Vertex`.
- `remove()` is `remove_vertex()` or `remove_edge()`, which say which they do.
- The options of `plot()` are named for what they are: `vopt` is `vertex_opt`,
`eopt` is `edge_opt`, `text` is `labels` and `text_opt`, `colorcomponents` is
`color_components` and `force2d` is `force_2d`. The positional order is
unchanged; giving both an old name and its new one is a `TypeError`.

Added

- `adjacent()` and `isadjacent()`, the graph-theory names, which have one
spelling. `neighbours()`, `neighbors()` and `isneighbour()` from 0.6.5 are
kept as aliases, with `isneighbor()` added, and none of them warn.
- `plot()` draws each edge once. A directed graph has arrowheads, which
`arrow_opt` styles; edges that join the same pair of vertices, in either
direction, are arcs that bow apart; and a loop is a circle at its vertex. In
0.6.5 a directed graph looked like an undirected one, parallel edges lay
exactly on top of each other and a loop was a line of no length. `plot()`
does the same in 3D, and `highlight_edge()` follows an arc.
- `Graph(directed=...)`, the `directed` property, and `vertex_class` and
`edge_class` constructor arguments, so that user subclasses of `Vertex` and
`Edge` are built by the graph.
- `add_edge()` accepts an `Edge`, and `connect()` an `edge=` argument.
- `edges_to(dest)` lists every edge from a vertex to another, so parallel edges
can be seen; `edgeto(dest)` is still the cheapest.
- Directed vertices record their incoming edges: `predecessors()`,
`successors()`, `in_edges()`, `out_edges()`, `in_degree` and `out_degree`.
- `export()` writes a graph as GraphViz dot, Mermaid, GraphML or ELK JSON, or as
an image rendered by the GraphViz executables, with the layout preserved. The
`pgraph.exporters` module can be extended with `register_exporter`.
- `validate()` reports every way in which a graph's records disagree, for tests
and debugging.
- A graph can be pickled, and `copy()` works for graphs of any size. `copy()`
raised `RecursionError` for a path of about 100 vertices.
- `iscyclic()`, `remove_edge()`, `remove_vertex()` and the `dim` argument, which
declares that every embedded vertex has a coordinate of exactly that length.
- Self-loops and parallel edges are supported throughout: adjacency, incidence,
degree and Laplacian matrices, and planning, which takes the cheapest of
parallel edges.
- `SE2Metric(weight)` is the `"SE2"` metric with a weight on the angle, the
length that a radian is worth, so that position and orientation need not
count equally. `"SE2"` is `SE2Metric()`, a weight of 1, so existing results
are unchanged. It can be the metric or the heuristic of a graph and is
pickled with its weight.
- Spatial queries on an embedded graph: `coordinates()` is the matrix of vertex
coordinates, a column per vertex; `within(box)` the vertices inside a box;
`near(point, radius)` those within a distance, nearest first; and
`subgraph(vertices)` the graph that a set of vertices induces, given as
vertices, names or a boolean mask, as a new graph that shares nothing with
the original.
- `build_kdtree()` makes an optional k-d tree that `closest()` and `near()` use
for the built-in metrics, `SE2Metric` included. It needs scipy, which is
imported only then and is not a dependency; and it is dropped whenever a
vertex is added or removed or a coordinate is assigned, so a stale one is
never used.
- `validate()` also checks that coordinates are finite vectors of one length,
and with `embedded=True` that every vertex has one.

Faster

- `path_BFS`, `path_UCS` and `path_Astar` scaled badly: they searched lists with
`in`, and took the minimum of the whole frontier at every step. Results and
the verbose output are identical. On a random graph of 10,000 vertices and
30,000 edges, the mean of six random queries is 3 ms for breadth-first search
where 0.6.5 took 172 ms, 18 ms for uniform cost where it took 1,500 ms, and
13 ms for A* where it took 410 ms.
- The components are found by a union-find over the edges, 4 ms for that graph
where 0.6.5 took 15 ms after a change.
- Creating a vertex is faster: `Vertex(coord)` takes 0.35 µs where 0.6.5 took
1.5 µs, and `g.add_vertex(coord)` 1.7 µs where it took 2.2 µs. Making the
coordinate with `np.r_` was most of the cost.
- `import pgraph` takes 0.04 s where it took 0.6 s, and loads neither
matplotlib, spatialmath nor scipy: they are imported by the methods that plot
or build a spatial index. A script that only builds graphs and plans paths no
longer pays for them.
- `str(g)` separates thousands: `DGraph: 4,881 vertices, 8,392 edges, 14
components`.

Fixed

- `Dict()` always created undirected vertices, even for a `DGraph`; `connect()`
joined vertices of two different graphs, and now also validates the
endpoints of a supplied edge; `path_BFS`/`path_UCS`/`path_Astar` checked the
type of the start vertex twice and never the goal; and `distance()` and the
planners packed `nan` or `None` into their results for edges with no cost
instead of raising.
- `nc` never returned for many directed graphs, the smallest a 4-cycle, and
neither did `component()`, `samecomponent()` or `plot()`, which ask for it.
Components of a directed graph are weakly connected, as they were meant to
be, and are numbered from 0 by the first vertex of each, for both kinds of
graph. They are also no longer found again by every call once the graph has
changed.
- `Edge.vertices()` raised `DeprecationWarning` as an exception; it now warns
and returns the endpoints, as `Edge.endpoints` does.
- `incidence()` returned the wrong matrix for every graph with an edge: a loop
variable overwrote the vertex index, so edges were marked in the wrong rows,
and for a directed graph the vertex at the end of an edge was not marked at
all. The columns follow `edges()`, which is now in the order the edges were
added, where 0.6.5 used the order of a set.
- `remove()` crashed for every directed edge; `dotfile()` closed `sys.stdout`
when called with its default arguments; and several methods raised a bare
`AttributeError` or `TypeError` instead of a clear message for a vertex with
no coordinate or no graph.
- `highlight_edge()`, and so `highlight_path()`, raised `AttributeError` for a
graph of three dimensions, because it drew a flat arrow in the 3D axes (0.6.5
drew lines), and `highlight_vertex()` put its marker at z = 0. They draw in
the dimension of the axes, so a 3D graph plotted with `force_2d` is
highlighted in 2D.
- Removing a vertex left its `_graph` pointing at the graph; renaming a vertex
left the name table stale; `g[x]` accepted any type.

Known limitations

- A graph whose metric is a `lambda` or a locally defined function can be copied
but not pickled; the built-in `"L1"`, `"L2"` and `"SE2"` and module-level
functions can be.

Build, tests and documentation

- Modern type hints and reST docstrings throughout, most with a runnable
example, and `mypy` is a CI check.
- The documentation is organized by task, with the data structures explained and
a policy page on object lifecycle, and builds with `sphinx-pyrunblock` (which
replaces the unmaintained `sphinx-autorun`) and the matplotlib plot directive.
- Python 3.9 to 3.14 are tested, the unused `scipy` dependency is dropped, and
PyPI publishing is automated through GitHub Releases and trusted publishing.
The workflow checks that the tag is the version in `pyproject.toml` and that
the tagged commit is on `main`, and installs the built wheel and runs a path
query before it uploads.
- The documentation site is deployed with the GitHub Pages actions. It had not
been updated since January 2025, because the CI pushed to a branch that Pages
no longer served.
- `llms.txt` and `llms-full.txt` are published with the documentation, for AI
assistants: a short introduction with a quick start and common mistakes, and the
whole documentation as one text file. `AGENTS.md` describes the source for
assistants that work on it.
- Two notebooks in `examples/`: `roads.ipynb`, the Queensland example, and
`london.ipynb`, the road network of central London from OpenStreetMap as a
directed graph with one-way streets, planned through with A*. There is also
a benchmark, `examples/benchmark.py`, and the README has its results and an
installation section.
- The Queensland figure in the README shows the shortest route. It had been
drawn with coordinates in metres and distances in kilometres, which made the
A* heuristic far too large, so the route it found, 1,759 km, was not the
shortest, 1,659 km.

0.6.3

- runs with Numpy2.x
Expand Down
1 change: 0 additions & 1 deletion RELEASE

This file was deleted.

Loading
Loading