Skip to content

Commit 3947a02

Browse files
authored
Merge pull request #14 from petercorke/chore/release-1.0.0
Release 1.0.0
2 parents b630230 + f06a1cc commit 3947a02

6 files changed

Lines changed: 202 additions & 1922 deletions

File tree

‎.github/workflows/publish.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,4 @@ jobs:
112112
# pinned to a commit, as this action can publish to PyPI; dependabot updates it
113113
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
114114
with:
115-
skip_existing: true
115+
skip-existing: true

‎CHANGELOG.md‎

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,203 @@
1+
1.0.0
2+
3+
First 1.0 release: six years of production use since the first commit in 2020
4+
(including as a real dependency of both the Robotics Toolbox for Python and the
5+
Machine Vision Toolbox for Python), and this cycle's type hints, documentation,
6+
testing and bug-fixing pass, felt like the right point to commit to API
7+
stability. This is the first release with a considered API, so it contains
8+
several deliberate breaking changes, listed first with how to migrate.
9+
10+
Breaking changes (from 0.6.5)
11+
12+
- The `UGraph`/`DGraph` constructor arguments are keyword-only. 0.6.5 accepted
13+
and silently ignored a first positional argument, so `UGraph(6)` looked as
14+
though it set a dimension but did nothing. Now it is a `TypeError`: write
15+
`UGraph(dim=6)`.
16+
- `degree` of a `DGraph` is the graph-theory degree, in plus out, so a
17+
self-loop counts 2 and the degrees sum to twice the number of edges. It used
18+
to count only the edges leaving the vertex. This changes `vertex.degree`,
19+
`graph.degree()` and `graph.connectivity()`, and `graph.average_degree()` is
20+
now 2E/N for both kinds of graph (it was E/N for a `DGraph`). The previous
21+
values are `vertex.out_degree`, `[v.out_degree for v in g]` and `g.ne / g.n`.
22+
`laplacian()` is unchanged (out-degree minus adjacency). Undirected graphs
23+
are unchanged.
24+
- `graph.edges()` is a `list` in the order the edges were added. It was the
25+
graph's own `set`, with no defined order, although documented as a list. Both
26+
`graph.edges()` and `vertex.edges()` return a new list, so changing the result
27+
cannot corrupt the graph. Use `set(g.edges())` for set operations.
28+
- `Edge.v1` and `Edge.v2` are read-only, and so is `Vertex.label` (the graph
29+
assigns it, and reading it now finds the components, where 0.6.5 gave `None`
30+
until `nc` had been asked for). Assigning an endpoint of an edge that was in a graph left the
31+
graph inconsistent and the edge impossible to remove. Give the vertices to
32+
`Edge(v1, v2)` or connect the edge with `v1.connect(v2, edge=e)`.
33+
- Assigning `Vertex.name` renames it in the graph (so `g[new]` works) and
34+
refuses a name already in use or `None`.
35+
- `add_vertex` raises `ValueError` for a name already used in the graph or a
36+
vertex that already belongs to a graph, instead of silently shadowing it.
37+
The default name `#N` is now the first free one, which could previously
38+
repeat after a removal.
39+
- There is one vertex class, `Vertex`: a vertex has no direction of its own, the
40+
graph it belongs to decides how an edge is recorded at its ends. `UVertex`
41+
and `DVertex` are deprecated names for it (below). A vertex given to
42+
`add_vertex` is no longer checked against the kind of graph, because there is
43+
no kind to mismatch. An override of the private `_attach`/`_detach` hooks
44+
moves from a vertex subclass to a `UGraph`/`DGraph` subclass.
45+
- The abstract base class `PGraph` is now `Graph`, and `Graph` and `Vertex` are
46+
exported from the package. `Graph` is concrete: `Graph(directed=True)` is a
47+
`DGraph` and `Graph()` a `UGraph`.
48+
- `Adjacency()` treats a non-zero diagonal element as a self-loop.
49+
- The `str` of an edge between vertices that are not in a graph shows `--`,
50+
because there is no kind of graph to say whether it is directed.
51+
52+
Deprecated (still work, with a `DeprecationWarning`)
53+
54+
- `Laplacian()` is `laplacian()`: a capital initial denotes a class method that
55+
builds a graph, like `Adjacency()` and `Dict()`.
56+
- `UVertex` and `DVertex` are `Vertex`.
57+
- `remove()` is `remove_vertex()` or `remove_edge()`, which say which they do.
58+
- The options of `plot()` are named for what they are: `vopt` is `vertex_opt`,
59+
`eopt` is `edge_opt`, `text` is `labels` and `text_opt`, `colorcomponents` is
60+
`color_components` and `force2d` is `force_2d`. The positional order is
61+
unchanged; giving both an old name and its new one is a `TypeError`.
62+
63+
Added
64+
65+
- `adjacent()` and `isadjacent()`, the graph-theory names, which have one
66+
spelling. `neighbours()`, `neighbors()` and `isneighbour()` from 0.6.5 are
67+
kept as aliases, with `isneighbor()` added, and none of them warn.
68+
- `plot()` draws each edge once. A directed graph has arrowheads, which
69+
`arrow_opt` styles; edges that join the same pair of vertices, in either
70+
direction, are arcs that bow apart; and a loop is a circle at its vertex. In
71+
0.6.5 a directed graph looked like an undirected one, parallel edges lay
72+
exactly on top of each other and a loop was a line of no length. `plot()`
73+
does the same in 3D, and `highlight_edge()` follows an arc.
74+
- `Graph(directed=...)`, the `directed` property, and `vertex_class` and
75+
`edge_class` constructor arguments, so that user subclasses of `Vertex` and
76+
`Edge` are built by the graph.
77+
- `add_edge()` accepts an `Edge`, and `connect()` an `edge=` argument.
78+
- `edges_to(dest)` lists every edge from a vertex to another, so parallel edges
79+
can be seen; `edgeto(dest)` is still the cheapest.
80+
- Directed vertices record their incoming edges: `predecessors()`,
81+
`successors()`, `in_edges()`, `out_edges()`, `in_degree` and `out_degree`.
82+
- `export()` writes a graph as GraphViz dot, Mermaid, GraphML or ELK JSON, or as
83+
an image rendered by the GraphViz executables, with the layout preserved. The
84+
`pgraph.exporters` module can be extended with `register_exporter`.
85+
- `validate()` reports every way in which a graph's records disagree, for tests
86+
and debugging.
87+
- A graph can be pickled, and `copy()` works for graphs of any size. `copy()`
88+
raised `RecursionError` for a path of about 100 vertices.
89+
- `iscyclic()`, `remove_edge()`, `remove_vertex()` and the `dim` argument, which
90+
declares that every embedded vertex has a coordinate of exactly that length.
91+
- Self-loops and parallel edges are supported throughout: adjacency, incidence,
92+
degree and Laplacian matrices, and planning, which takes the cheapest of
93+
parallel edges.
94+
- `SE2Metric(weight)` is the `"SE2"` metric with a weight on the angle, the
95+
length that a radian is worth, so that position and orientation need not
96+
count equally. `"SE2"` is `SE2Metric()`, a weight of 1, so existing results
97+
are unchanged. It can be the metric or the heuristic of a graph and is
98+
pickled with its weight.
99+
- Spatial queries on an embedded graph: `coordinates()` is the matrix of vertex
100+
coordinates, a column per vertex; `within(box)` the vertices inside a box;
101+
`near(point, radius)` those within a distance, nearest first; and
102+
`subgraph(vertices)` the graph that a set of vertices induces, given as
103+
vertices, names or a boolean mask, as a new graph that shares nothing with
104+
the original.
105+
- `build_kdtree()` makes an optional k-d tree that `closest()` and `near()` use
106+
for the built-in metrics, `SE2Metric` included. It needs scipy, which is
107+
imported only then and is not a dependency; and it is dropped whenever a
108+
vertex is added or removed or a coordinate is assigned, so a stale one is
109+
never used.
110+
- `validate()` also checks that coordinates are finite vectors of one length,
111+
and with `embedded=True` that every vertex has one.
112+
113+
Faster
114+
115+
- `path_BFS`, `path_UCS` and `path_Astar` scaled badly: they searched lists with
116+
`in`, and took the minimum of the whole frontier at every step. Results and
117+
the verbose output are identical. On a random graph of 10,000 vertices and
118+
30,000 edges, the mean of six random queries is 3 ms for breadth-first search
119+
where 0.6.5 took 172 ms, 18 ms for uniform cost where it took 1,500 ms, and
120+
13 ms for A* where it took 410 ms.
121+
- The components are found by a union-find over the edges, 4 ms for that graph
122+
where 0.6.5 took 15 ms after a change.
123+
- Creating a vertex is faster: `Vertex(coord)` takes 0.35 µs where 0.6.5 took
124+
1.5 µs, and `g.add_vertex(coord)` 1.7 µs where it took 2.2 µs. Making the
125+
coordinate with `np.r_` was most of the cost.
126+
- `import pgraph` takes 0.04 s where it took 0.6 s, and loads neither
127+
matplotlib, spatialmath nor scipy: they are imported by the methods that plot
128+
or build a spatial index. A script that only builds graphs and plans paths no
129+
longer pays for them.
130+
- `str(g)` separates thousands: `DGraph: 4,881 vertices, 8,392 edges, 14
131+
components`.
132+
133+
Fixed
134+
135+
- `Dict()` always created undirected vertices, even for a `DGraph`; `connect()`
136+
joined vertices of two different graphs, and now also validates the
137+
endpoints of a supplied edge; `path_BFS`/`path_UCS`/`path_Astar` checked the
138+
type of the start vertex twice and never the goal; and `distance()` and the
139+
planners packed `nan` or `None` into their results for edges with no cost
140+
instead of raising.
141+
- `nc` never returned for many directed graphs, the smallest a 4-cycle, and
142+
neither did `component()`, `samecomponent()` or `plot()`, which ask for it.
143+
Components of a directed graph are weakly connected, as they were meant to
144+
be, and are numbered from 0 by the first vertex of each, for both kinds of
145+
graph. They are also no longer found again by every call once the graph has
146+
changed.
147+
- `Edge.vertices()` raised `DeprecationWarning` as an exception; it now warns
148+
and returns the endpoints, as `Edge.endpoints` does.
149+
- `incidence()` returned the wrong matrix for every graph with an edge: a loop
150+
variable overwrote the vertex index, so edges were marked in the wrong rows,
151+
and for a directed graph the vertex at the end of an edge was not marked at
152+
all. The columns follow `edges()`, which is now in the order the edges were
153+
added, where 0.6.5 used the order of a set.
154+
- `remove()` crashed for every directed edge; `dotfile()` closed `sys.stdout`
155+
when called with its default arguments; and several methods raised a bare
156+
`AttributeError` or `TypeError` instead of a clear message for a vertex with
157+
no coordinate or no graph.
158+
- `highlight_edge()`, and so `highlight_path()`, raised `AttributeError` for a
159+
graph of three dimensions, because it drew a flat arrow in the 3D axes (0.6.5
160+
drew lines), and `highlight_vertex()` put its marker at z = 0. They draw in
161+
the dimension of the axes, so a 3D graph plotted with `force_2d` is
162+
highlighted in 2D.
163+
- Removing a vertex left its `_graph` pointing at the graph; renaming a vertex
164+
left the name table stale; `g[x]` accepted any type.
165+
166+
Known limitations
167+
168+
- A graph whose metric is a `lambda` or a locally defined function can be copied
169+
but not pickled; the built-in `"L1"`, `"L2"` and `"SE2"` and module-level
170+
functions can be.
171+
172+
Build, tests and documentation
173+
174+
- Modern type hints and reST docstrings throughout, most with a runnable
175+
example, and `mypy` is a CI check.
176+
- The documentation is organized by task, with the data structures explained and
177+
a policy page on object lifecycle, and builds with `sphinx-pyrunblock` (which
178+
replaces the unmaintained `sphinx-autorun`) and the matplotlib plot directive.
179+
- Python 3.9 to 3.14 are tested, the unused `scipy` dependency is dropped, and
180+
PyPI publishing is automated through GitHub Releases and trusted publishing.
181+
The workflow checks that the tag is the version in `pyproject.toml` and that
182+
the tagged commit is on `main`, and installs the built wheel and runs a path
183+
query before it uploads.
184+
- The documentation site is deployed with the GitHub Pages actions. It had not
185+
been updated since January 2025, because the CI pushed to a branch that Pages
186+
no longer served.
187+
- `llms.txt` and `llms-full.txt` are published with the documentation, for AI
188+
assistants: a short introduction with a quick start and common mistakes, and the
189+
whole documentation as one text file. `AGENTS.md` describes the source for
190+
assistants that work on it.
191+
- Two notebooks in `examples/`: `roads.ipynb`, the Queensland example, and
192+
`london.ipynb`, the road network of central London from OpenStreetMap as a
193+
directed graph with one-way streets, planned through with A*. There is also
194+
a benchmark, `examples/benchmark.py`, and the README has its results and an
195+
installation section.
196+
- The Queensland figure in the README shows the shortest route. It had been
197+
drawn with coordinates in metres and distances in kilometres, which made the
198+
A* heuristic far too large, so the route it found, 1,759 km, was not the
199+
shortest, 1,659 km.
200+
1201
0.6.3
2202

3203
- runs with Numpy2.x

‎RELEASE‎

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)