From a966001b5bcfaf156a9dcc5f85092a61eb9b227e Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Thu, 3 Sep 2026 20:33:56 +0200 Subject: [PATCH 01/14] Release 1.0.0 Six years of production use (first commit 2020-09-20), a real downstream dependency (RTB), and this cycle's comprehensive type-hinting/documentation/bug-fixing pass felt like the right point to commit to API stability. See CHANGELOG.md for the full list of what changed since 0.6.5 (the last real release -- 0.6.4 and 0.6.5 were identical except for the version string). Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b00fd65..c65366cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,47 @@ +1.0.0 + +- First 1.0 release: six years of production use since the first commit + in 2020 (including as a real dependency of the Robotics Toolbox for + Python), and this cycle's comprehensive type-hinting, documentation, + and bug-fixing pass, felt like the right point to commit to API + stability. +- Breaking (internal-only): renamed the abstract base classes `PGraph` -> + `_BaseGraph` and `Vertex` -> `BaseVertex`. Neither was ever exported + from the package or referenced by name elsewhere in the ecosystem. +- Added `iscyclic()` to test whether a graph contains a cycle. +- Added a `dim` keyword arg to `UGraph`/`DGraph`: declares that every + embedded vertex must have a coordinate of exactly that length, + enforced in `add_vertex()`. +- Added `remove_edge()`/`remove_vertex()` as explicit replacements for + the old, ambiguous `remove()` (deprecated, still works). +- Added modern type hints and reST docstrings throughout, most with a + runnable, doc-build-verified usage example. +- Fixed a cluster of real bugs found via the type-hinting and testing + pass: wrong-subclass vertices silently accepted or mangled by + `add_vertex()`; `Dict()` always creating undirected vertices even for + `DGraph`; a broken same-type check in `connect()` that never actually + caught a cross-type or cross-graph connection; `path_BFS`/`path_UCS`/ + `path_Astar` validating the wrong parameter's type; `distance()` and + the path planners silently packing `None`/`nan` into results for + costless edges instead of raising; two bugs in `incidence()` that + scrambled its output; `remove()` unconditionally crashing for any + directed-graph edge; `dotfile()` closing `sys.stdout` when called + with its default arguments; and several methods crashing with a bare + `AttributeError`/`TypeError` instead of a clear message when called on + a vertex with no coordinate or no graph membership. +- Removed the vertex `connect()` "pass an existing Edge instead of a + destination vertex" option -- it was completely broken and exercised + by no test. +- `mypy` added as an informational CI check; the bulk of the + `Optional`-narrowing gap it found has been closed. +- Documentation now builds with `sphinx-pyrunblock` (replacing the + unmaintained `sphinx-autorun`) and `matplotlib.sphinxext.plot_directive` + for real embedded plot images; the doc footer now matches the + ecosystem-wide convention. +- CI: added Python 3.13/3.14 to the test matrix; PyPI publishing is now + automated via GitHub Releases and trusted publishing (see + `.github/workflows/publish.yml`) instead of a manual `twine upload`. + 0.6.3 - runs with Numpy2.x diff --git a/pyproject.toml b/pyproject.toml index a5b133ad..a3884f35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pgraph-python" -version = "0.6.4" +version = "1.0.0" authors = [{ name = "Peter Corke", email = "rvc@petercorke.com" }] description = "Mathematical graphs for Python" readme = "README.md" From dc6e83708296b6f46ec1d6192e15b9ae80b0ee08 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Thu, 3 Sep 2026 20:34:35 +0200 Subject: [PATCH 02/14] changelog: also credit MVTB as a real dependency, not just RTB Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c65366cb..8cf07d8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ 1.0.0 - First 1.0 release: six years of production use since the first commit - in 2020 (including as a real dependency of the Robotics Toolbox for - Python), and this cycle's comprehensive type-hinting, documentation, - and bug-fixing pass, felt like the right point to commit to API - stability. + 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 comprehensive type-hinting, documentation, and bug-fixing + pass, felt like the right point to commit to API stability. - Breaking (internal-only): renamed the abstract base classes `PGraph` -> `_BaseGraph` and `Vertex` -> `BaseVertex`. Neither was ever exported from the package or referenced by name elsewhere in the ecosystem. From 5bfbb8b44f4c377b3238007856f3d1f35774d9df Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Mon, 12 Oct 2026 12:24:36 +1000 Subject: [PATCH 03/14] changelog: write the 1.0.0 entry for what has landed Breaking changes with how to migrate, deprecations, additions, fixes, and known limitations. Each before-and-after was checked by running it against the real 0.6.5 code, which corrected three statements: adjacent() did not exist in 0.6.5 (so the alias change is additive), Edge.vertices() raised rather than warned, and the old wrong-subclass claim no longer applies with one vertex class. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 157 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 116 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cf07d8f..661d5fc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,46 +1,121 @@ 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 comprehensive type-hinting, documentation, and bug-fixing - pass, felt like the right point to commit to API stability. -- Breaking (internal-only): renamed the abstract base classes `PGraph` -> - `_BaseGraph` and `Vertex` -> `BaseVertex`. Neither was ever exported - from the package or referenced by name elsewhere in the ecosystem. -- Added `iscyclic()` to test whether a graph contains a cycle. -- Added a `dim` keyword arg to `UGraph`/`DGraph`: declares that every - embedded vertex must have a coordinate of exactly that length, - enforced in `add_vertex()`. -- Added `remove_edge()`/`remove_vertex()` as explicit replacements for - the old, ambiguous `remove()` (deprecated, still works). -- Added modern type hints and reST docstrings throughout, most with a - runnable, doc-build-verified usage example. -- Fixed a cluster of real bugs found via the type-hinting and testing - pass: wrong-subclass vertices silently accepted or mangled by - `add_vertex()`; `Dict()` always creating undirected vertices even for - `DGraph`; a broken same-type check in `connect()` that never actually - caught a cross-type or cross-graph connection; `path_BFS`/`path_UCS`/ - `path_Astar` validating the wrong parameter's type; `distance()` and - the path planners silently packing `None`/`nan` into results for - costless edges instead of raising; two bugs in `incidence()` that - scrambled its output; `remove()` unconditionally crashing for any - directed-graph edge; `dotfile()` closing `sys.stdout` when called - with its default arguments; and several methods crashing with a bare - `AttributeError`/`TypeError` instead of a clear message when called on - a vertex with no coordinate or no graph membership. -- Removed the vertex `connect()` "pass an existing Edge instead of a - destination vertex" option -- it was completely broken and exercised - by no test. -- `mypy` added as an informational CI check; the bulk of the - `Optional`-narrowing gap it found has been closed. -- Documentation now builds with `sphinx-pyrunblock` (replacing the - unmaintained `sphinx-autorun`) and `matplotlib.sphinxext.plot_directive` - for real embedded plot images; the doc footer now matches the - ecosystem-wide convention. -- CI: added Python 3.13/3.14 to the test matrix; PyPI publishing is now - automated via GitHub Releases and trusted publishing (see - `.github/workflows/publish.yml`) instead of a manual `twine upload`. +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). 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. + +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. +- `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. + +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. +- `Edge.vertices()` raised `DeprecationWarning` as an exception; it now warns + and returns the endpoints, as `Edge.endpoints` does. +- Two bugs in `incidence()` that scrambled its output; `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. +- 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 + +- `plot()` draws no arrowheads for a directed graph, and parallel edges and + self-loops are not visible in it. +- 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. 0.6.3 From b12b68896e60f25565ef9262da1f8788559be698 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Mon, 12 Oct 2026 12:31:21 +1000 Subject: [PATCH 04/14] changelog: say exactly what was wrong with incidence() Checked against 0.6.5 on undirected and directed paths and stars: the matrix was wrong in all four, from a loop variable that overwrote the vertex index and, for a directed graph, from visiting only each vertex's outgoing edges. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 661d5fc7..26a9be93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,11 +91,15 @@ Fixed instead of raising. - `Edge.vertices()` raised `DeprecationWarning` as an exception; it now warns and returns the endpoints, as `Edge.endpoints` does. -- Two bugs in `incidence()` that scrambled its output; `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. +- `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. - Removing a vertex left its `_graph` pointing at the graph; renaming a vertex left the name table stale; `g[x]` accepted any type. From 26070bf2c90a741a266c02b72637ea95e23c6a62 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Mon, 12 Oct 2026 13:39:58 +1000 Subject: [PATCH 05/14] changelog: plot(), the components hang and the label plot() now draws a directed graph with arrowheads, bows parallel and antiparallel edges apart and draws loops, so it is no longer a known limitation. Adds the plot() option renames to the deprecations, nc not returning for many directed graphs and label being None until nc had been asked for to what is fixed. Each was checked against the real 0.6.5. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26a9be93..f9a4ad8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,8 @@ Breaking changes (from 0.6.5) `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). Assigning an endpoint of an edge that was in a graph left the + 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 @@ -54,12 +55,22 @@ Deprecated (still work, with a `DeprecationWarning`) 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. @@ -89,6 +100,12 @@ Fixed 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 @@ -105,8 +122,6 @@ Fixed Known limitations -- `plot()` draws no arrowheads for a directed graph, and parallel edges and - self-loops are not visible in it. - 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. From 989fbfd9885ce15965de655014fb2fc0b37e6e61 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Tue, 13 Oct 2026 10:45:41 +1000 Subject: [PATCH 06/14] changelog: SE2Metric, the spatial queries, the speed-ups, the examples Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9a4ad8e..da57ed3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,37 @@ Added - 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. For one query on 10,000 vertices, + breadth-first search takes 3 ms where it took 145 ms, uniform cost 7 ms + where it took 160 ms, and A* 3 ms where it took 17 ms. +- The components are found by a union-find over the edges: 4 ms for 10,000 + vertices where it was 9 ms, and 40 ms for 100,000 where it was 136 ms. +- Creating a vertex is about 20% faster. +- `str(g)` separates thousands: `DGraph: 4,881 vertices, 8,392 edges, 14 + components`. Fixed @@ -135,6 +166,18 @@ Build, tests and documentation 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. +- 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 From d552e5bbfc1bc5885bd2e656e0ef011c12126a41 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Tue, 13 Oct 2026 10:45:41 +1000 Subject: [PATCH 07/14] ci: skip-existing, the kebab-case name of the input The action has normalized its inputs; skip_existing is deprecated. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 213c7d39..6d0f1da2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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 From 4b325b4f1000a6c785cf01054a63be816e04df43 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Tue, 13 Oct 2026 10:46:47 +1000 Subject: [PATCH 08/14] changelog: speed-ups measured against 0.6.5, and the cost of add_vertex Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da57ed3d..9a32fff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -114,12 +114,12 @@ 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. For one query on 10,000 vertices, - breadth-first search takes 3 ms where it took 145 ms, uniform cost 7 ms - where it took 160 ms, and A* 3 ms where it took 17 ms. -- The components are found by a union-find over the edges: 4 ms for 10,000 - vertices where it was 9 ms, and 40 ms for 100,000 where it was 136 ms. -- Creating a vertex is about 20% faster. + 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. - `str(g)` separates thousands: `DGraph: 4,881 vertices, 8,392 edges, 14 components`. @@ -153,6 +153,8 @@ Fixed Known limitations +- Adding a vertex takes about 2 µs, where 0.6.5 took 1.3 µs: it now checks the + name, the dimension and that the vertex is in no graph. - 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. From f75f3208eb74bf211e4d7169e9c9bad4c7009857 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Tue, 13 Oct 2026 10:54:54 +1000 Subject: [PATCH 09/14] changelog: creating a vertex is faster than 0.6.5 Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a32fff8..7734e1ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,9 @@ Faster 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. - `str(g)` separates thousands: `DGraph: 4,881 vertices, 8,392 edges, 14 components`. @@ -153,8 +156,6 @@ Fixed Known limitations -- Adding a vertex takes about 2 µs, where 0.6.5 took 1.3 µs: it now checks the - name, the dimension and that the vertex is in no graph. - 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. From e1ff1f79847887644e795b239707d95569fdb3b1 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Tue, 13 Oct 2026 11:20:15 +1000 Subject: [PATCH 10/14] changelog: llms.txt and AGENTS.md Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7734e1ad..d7a7b465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -172,6 +172,10 @@ Build, tests and documentation 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. +- `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 From ccf3934f087129cadf1d443c0e1f072a1fd25ce9 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Tue, 13 Oct 2026 11:28:58 +1000 Subject: [PATCH 11/14] changelog: the documentation site is deployed again Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7a7b465..220c928c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -172,6 +172,9 @@ Build, tests and documentation 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 From 7adb665b8b2dc451cf2703f356950d3150b65d73 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Tue, 13 Oct 2026 12:11:27 +1000 Subject: [PATCH 12/14] changelog: import pgraph is 0.04 s, highlighting in 3D Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 220c928c..cf35675b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -123,6 +123,10 @@ Faster - 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`. @@ -151,6 +155,11 @@ Fixed 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. From bc647b959b9265710462e713bdc82363fd1dbb70 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Tue, 13 Oct 2026 12:14:48 +1000 Subject: [PATCH 13/14] Remove RELEASE A leftover of the MATLAB version that held 0.6.1. Nothing reads it; the version is in pyproject.toml. Co-Authored-By: Claude Sonnet 5.5 --- RELEASE | 1 - 1 file changed, 1 deletion(-) delete mode 100644 RELEASE diff --git a/RELEASE b/RELEASE deleted file mode 100644 index ee6cdce3..00000000 --- a/RELEASE +++ /dev/null @@ -1 +0,0 @@ -0.6.1 From f06a1cc8ee1d879a7877e4abb5330e2d3d8da45e Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Tue, 13 Oct 2026 12:15:38 +1000 Subject: [PATCH 14/14] Remove build/lib, a stale copy of the source Two files of an old setuptools build were committed with the move to the src layout, and were shipped in the sdist next to the real source. Co-Authored-By: Claude Sonnet 5.5 --- build/lib/pgraph/PGraph.py | 1918 ---------------------------------- build/lib/pgraph/__init__.py | 1 - 2 files changed, 1919 deletions(-) delete mode 100644 build/lib/pgraph/PGraph.py delete mode 100644 build/lib/pgraph/__init__.py diff --git a/build/lib/pgraph/PGraph.py b/build/lib/pgraph/PGraph.py deleted file mode 100644 index e0c7a48a..00000000 --- a/build/lib/pgraph/PGraph.py +++ /dev/null @@ -1,1918 +0,0 @@ -from abc import ABC -import sys -import numpy as np -import matplotlib.pyplot as plt -import copy -from collections.abc import Iterable -import tempfile -import subprocess -import webbrowser - -from spatialmath.base.graphics import axes_logic - - -class PGraph(ABC): - - def __init__(self, arg=None, metric=None, heuristic=None, verbose=False): - # we use a list and a dict, the list respects the order of adding - self._vertexlist = [] - self._vertexdict = {} - self._edgelist = set() - self._verbose = verbose - if metric is None: - self.metric = "L2" - else: - self.metric = metric - if heuristic is None: - self.heuristic = self.metric - else: - self.heuristic = heuristic - - def __str__(self): - s = f"{self.__class__.__name__}: {self.n} {'vertex' if self.n==1 else 'vertices'}, {self.ne} edge{'s'[:self.ne^1]}, {self.nc} component{'s'[:self.nc^1]}" - return s - - def __repr__(self): - return str(self) - - @classmethod - def Dict(cls, d, reverse=False): - """ - Create graph from parent/child dictionary - - :param d: dictionary that maps from ``Vertex`` subclass to ``Vertex`` subclass - :type d: dict - :param reverse: reverse link direction, defaults to False - :type reverse: bool, optional - :return: graph - :rtype: UGraph or DGraph - - Behaves like a constructor for a ``DGraph`` or ``UGraph`` from a - dictionary that maps vertices to parents. From this information it - can create a tree graph. - - By default parent vertices are linked their children. If ``reverse`` is - True then children are linked to their parents. - """ - - g = cls() - - for vertex, parent in d.items(): - if isinstance(vertex, str): - vertex_name = vertex - else: - vertex_name = vertex.name - - if vertex_name in g: - vertex = g[vertex_name] - else: - vertex = g.add_vertex(UVertex(), name=vertex_name) - - if isinstance(parent, str): - parent_name = parent - else: - parent_name = parent.name - if parent_name in g: - parent = g[parent_name] - else: - parent = g.add_vertex(UVertex(), name=parent_name) - - if reverse: - g.add_edge(vertex, parent) - else: - g.add_edge(parent, vertex) - - return g - - @classmethod - def Adjacency(cls, A, coords=None, names=None): - """ - Create graph from adjacency matrix - - :param A: adjacency matrix - :type A: ndarray(N,N) - :param coords: coordinates of vertices, defaults to None - :type coords: ndarray(N,M), optional - :param names: names of vertices, defaults to None - :type names: list(N) of str, optional - - :return: [description] - :rtype: [type] - - Create a directed or undirected graph where non-zero elements ``A[i,j]`` - correspond to edges from vertex ``i`` to vertex ``j``. - - .. warning:: For undirected graph ``A`` should be symmetric but this - is not checked. Only the upper triangular part is used. - """ - - if A.shape[0] != A.shape[1]: - raise ValueError("Adjacency matrix must be square") - if names is not None and len(names) != A.shape[0]: - raise ValueError("length of names must match dimension of adjacency matrix") - if coords is not None and coords.shape[0] != A.shape[0]: - raise ValueError("coords must have same number of rows as adjacency matrix") - - g = cls() - - name = None - coord = None - for i in range(A.shape[0]): - if names is not None: - name = names[i] - if coords is not None: - coord = coords[i, :] - g.add_vertex(name=name, coord=coord) - - if isinstance(g, UGraph): - # undirected graph - for i in range(A.shape[0]): - for j in range(i + 1, A.shape[1]): - if A[i, j] > 0: - g[i].connect(g[j], cost=A[i, j]) - else: - # directed graph - for i in range(A.shape[0]): - for j in range(A.shape[1]): - if A[i, j] > 0: - if i == j: - raise ValueError("loops in graph not supported") - g[i].connect(g[j], cost=A[i, j]) - - return g - - def copy(self): - """ - Deepcopy of graph - - :param g: A graph - :type g: PGraph - :return: deep copy - :rtype: PGraph - """ - return copy.deepcopy(self) - - def add_vertex(self, vertex, name=None): - """ - Add a vertex to the graph (superclass method) - - :param vertex: vertex to add - :type vertex: Vertex subclass - :param name: name of vertex - :type name: str - - ``G.add_vertex(v)`` add vertex ``v`` to the graph ``G``. - - If the vertex has no name and ``name`` is None give it a default name - ``#N`` where ``N`` is a consecutive integer. - - The vertex is placed into a dictionary with a key equal to its name. - """ - if name is None: - name = vertex.name - if name is None: - name = f"#{len(self._vertexlist)}" - vertex.name = name - self._vertexlist.append(vertex) - self._vertexdict[vertex.name] = vertex - if self._verbose: - print(f"New vertex {vertex.name}: {vertex.coord}") - vertex._graph = self - self._connectivitychange = True - return vertex - - def add_edge(self, v1, v2, **kwargs): - """ - Add an edge to the graph (superclass method) - - :param v1: first vertex (start if a directed graph) - :type v1: Vertex subclass - :param v2: second vertex (end if a directed graph) - :type v2: Vertex subclass - :param kwargs: optional arguments to pass to ``Vertex.connect`` - :return: edge - :rtype: Edge - - Create an edge between a vertex pair and adds it to the graph. - - This is a graph centric way of creating an edge. The - alternative is the ``connect`` method of a vertex. - - :seealso: :meth:`Edge.connect` :meth:`Vertex.connect` - """ - if isinstance(v1, str): - v1 = self[v1] - elif not isinstance(v1, Vertex): - raise TypeError("v1 must be Vertex subclass or string name") - if isinstance(v2, str): - v2 = self[v2] - elif not isinstance(v2, Vertex): - raise TypeError("v2 must be Vertex subclass or string name") - - if self._verbose: - print(f"New edge from {v1.name} to {v2.name}") - return v1.connect(v2, **kwargs) - - def remove(self, x): - """ - Remove element from graph (superclass method) - - :param x: element to remove from graph - :type x: Edge or Vertex subclass - :raises TypeError: unknown type - - The edge or vertex is removed, and all references and lists are - updated. - - .. warning:: The connectivity of the network may be changed. - """ - if isinstance(x, Edge): - # remove an edge - - # remove edge from the edgelist of connected vertices - x.v1._edgelist.remove(x) - x.v2._edgelist.remove(x) - - # indicate that connectivity has changed - x.v1._connectivitychange = True - x.v2._connectivitychange = True - self._connectivitychange = True - - # remove references to the vertices - x.v1 = None - x.v2 = None - - # remove from list of all edges - self._edgelist.remove(x) - - elif isinstance(x, Vertex): - # remove a vertex - - # remove all edges of this vertex - for edge in copy.copy(x._edgelist): - self.remove(edge) - - # remove from list and dict of all edges - self._vertexlist.remove(x) - del self._vertexdict[x.name] - else: - raise TypeError("expecting Edge or Vertex") - - def show(self): - print("vertices:") - for v in self._vertexlist: - print(" " + str(v)) - print("edges:") - for e in self._edgelist: - print(" " + str(e)) - - @property - def n(self): - """ - Number of vertices - - :return: Number of vertices - :rtype: int - """ - return len(self._vertexdict) - - @property - def ne(self): - """ - Number of edges - - :return: Number of vertices - :rtype: int - """ - return len(self._edgelist) - - @property - def nc(self): - """ - Number of components - - :return: Number of components - :rtype: int - - .. note:: - - - Components are labeled from 0 to ``g.nc-1``. - - A graph coloring algorithm is run if the graph connectivity - has changed. - - .. note:: A lazy approach is used, and if a connectivity changing - operation has been performed since the last call, the graph - coloring algorithm is run which is potentially expensive for - a large graph. - """ - self._graphcolor() - return self._ncomponents - - def _metricfunc(self, metric): - - def L1(v): - return np.linalg.norm(v, 1) - - def L2(v): - return np.linalg.norm(v) - - def SE2(v): - # wrap angle to range [-pi, pi) - v[2] = (v[2] + np.pi) % (2 * np.pi) - np.pi - return np.linalg.norm(v) - - if callable(metric): - return metric - elif isinstance(metric, str): - if metric == "L1": - return L1 - elif metric == "L2": - return L2 - elif metric == "SE2": - return SE2 - else: - raise ValueError("unknown metric") - - @property - def metric(self): - """ - Get the distance metric for graph - - :return: distance metric - :rtype: callable - - This is a function of a vector and returns a scalar. - """ - return self._metric - - @metric.setter - def metric(self, metric): - r""" - Set the distance metric for graph - - :param metric: distance metric - :type metric: callable or str - - This is a function of a vector and returns a scalar. It can be - user defined function or a string: - - - 'L1' is the norm :math:`L_1 = \Sigma_i | v_i |` - - 'L2' is the norm :math:`L_2 = \sqrt{ \Sigma_i v_i^2}` - - 'SE2' is a mixed norm for vectors :math:`(x, y, \theta)` and - is :math:`\sqrt{x^2 + y^2 + \bar{\theta}^2}` where :math:`\bar{\theta}` - is :math:`\theta` wrapped to the interval :math:`[-\pi, \pi)` - - The metric is used by :meth:`closest` and :meth:`distance` - """ - self._metric = self._metricfunc(metric) - - @property - def heuristic(self): - """ - Get the heuristic distance metric for graph - - :return: heuristic distance metric - :rtype: callable - - This is a function of a vector and returns a scalar. - """ - return self._heuristic - - @heuristic.setter - def heuristic(self, heuristic): - r""" - Set the heuristic distance metric for graph - - :param metric: heuristic distance metric - :type metric: callable or str - - This is a function of a vector and returns a scalar. It can be - user defined function or a string: - - - 'L1' is the norm :math:`L_1 = \Sigma_i | v_i |` - - 'L2' is the norm :math:`L_2 = \sqrt{ \Sigma_i v_i^2}` - - 'SE2' is a mixed norm for vectors :math:`(x, y, \theta)` and - is :math:`\sqrt{x^2 + y^2 + \bar{\theta}^2}` where :math:`\bar{\theta}` - is :math:`\theta` wrapped to the interval :math:`[-\pi, \pi)` - - The heuristic distance is only used by the A* planner :meth:`path_Astar`. - """ - self._heuristic = self._metricfunc(heuristic) - - def __repr__(self): - s = [] - for vertex in self: - ss = f"{vertex.name} at {vertex.coord}" - if vertex.label is not None: - ss += " component={vertex.label}" - s.append(ss) - return "\n".join(s) - - def __getitem__(self, i): - """ - Get vertex (superclass method) - - :param i: vertex description - :type i: int or str - :return: the referenced vertex - :rtype: Vertex subclass - - Retrieve a vertex by index or name: - - -``g[i]`` is the i'th vertex in the graph. This reflects the order of - addition to the graph. - -``g[s]`` is vertex named ``s`` - -``g[v]`` is ``v`` where ``v`` is a ``Vertex`` subclass - - This method also supports iteration over the vertices in a graph:: - - for v in g: - print(v) - - will iterate over all the vertices. - """ - if isinstance(i, int): - return self._vertexlist[i] - elif isinstance(i, str): - return self._vertexdict[i] - elif isinstance(i, Vertex): - return i - - def __contains__(self, item): - """ - Test if vertex in graph - - :param item: vertex or name of vertex - :type item: Vertex subclass or str - :return: true if vertex exists in the graph - :rtype: bool - - - ``'name' in graph`` is true if a vertex named ``'name'`` exists in the - graph. - - ``v in graph`` is true if the vertex reference ``v`` exists in the - graph. - - """ - if isinstance(item, str): - return item in self._vertexdict - elif isinstance(item, Vertex): - return item in self._vertexdict.values() - - def closest(self, coord): - """ - Vertex closest to point - - :param coord: coordinates of a point - :type coord: ndarray(n) - :return: closest vertex - :rtype: Vertex subclass - - Returns the vertex closest to the given point. Distance is computed - according to the graph's metric. - - :seealso: :meth:`metric` - """ - min_dist = np.inf - min_which = None - - for vertex in self: - d = self.metric(vertex.coord - coord) - if d < min_dist: - min_dist = d - min_which = vertex - - return min_which, min_dist - - def edges(self): - """ - Get all edges in graph (superclass method) - - :return: All edges in the graph - :rtype: list of Edge references - - We can iterate over all edges in the graph by:: - - for e in g.edges(): - print(e) - - .. note:: The ``edges()`` of a Vertex is a list of all edges connected - to that vertex. - - :seealso: :meth:`Vertex.edges` - """ - return self._edgelist - - def plot( - self, - colorcomponents=True, - force2d=False, - vopt={}, - eopt={}, - text={}, - block=None, - grid=True, - ax=None, - ): - """ - Plot the graph - - :param vopt: vertex format, defaults to 12pt o-marker - :type vopt: dict, optional - :param eopt: edge format, defaults to None - :type eopt: dict, optional - :param text: text label format, defaults to None - :type text: False or dict, optional - :param colorcomponents: color vertices and edges by component, defaults to None - :type color: bool, optional - :param block: block until figure is dismissed, defaults to True - :type block: bool, optional - - The graph is plotted using matplotlib. - - If ``colorcomponents`` is True then each component is assigned a unique - color. ``vertex`` and ``edge`` cannot include a color keyword. - - If ``text`` is a dict it is used to format text labels for the vertices - which are the vertex names. If ``text`` is None default formatting is - used. If ``text`` is False no labels are added. - """ - vopt = {**dict(marker="o", markersize=12), **vopt} - eopt = {**dict(linewidth=3), **eopt} - - if colorcomponents: - color = plt.cm.coolwarm(np.linspace(0, 1, self.nc)) - - if len(self[0].coord) == 2 or force2d: - # 2D plotting - if ax is None: - ax = axes_logic(ax, 2) - for c in range(self.nc): - # for each component - for vertex in self.component(c): - if text is not False: - ax.text(vertex.x, vertex.y, " " + vertex.name, **text) - if colorcomponents: - ax.plot(vertex.x, vertex.y, color=color[c, :], **vopt) - for v in vertex.neighbours(): - ax.plot( - [vertex.x, v.x], - [vertex.y, v.y], - color=color[c, :], - **eopt, - ) - else: - ax.plot(vertex.x, vertex.y, **vopt) - for v in vertex.neighbours(): - ax.plot([vertex.x, v.x], [vertex.y, v.y], **eopt) - else: - # 3D or higher plotting, just do (x, y, z) - if ax is None: - ax = axes_logic(ax, 3) - for c in range(self.nc): - # for each component - for vertex in self.component(c): - if text is not False: - ax.text( - vertex.x, vertex.y, vertex.z, " " + vertex.name, **text - ) - if colorcomponents: - ax.plot( - vertex.x, - vertex.y, - vertex.z, - **{**dict(color=color[c, :]), **vopt}, - ) - for v in vertex.neighbours(): - ax.plot( - [vertex.x, v.x], - [vertex.y, v.y], - [vertex.z, v.z], - **{**dict(color=color[c, :]), **eopt}, - ) - else: - ax.plot(vertex.x, vertex.y, **vopt) - for v in vertex.neighbours(): - ax.plot( - [vertex.x, v.x], - [vertex.y, v.y], - [vertex.z, v.z], - **eopt, - ) - # if nc > 1: - # # add a colorbar - # plt.colorbar() - ax.grid(grid) - if block is not None: - plt.show(block=block) - - def highlight_path(self, path, block=False, **kwargs): - """ - Highlight a path through the graph - - :param path: [description] - :type path: [type] - :param block: [description], defaults to True - :type block: bool, optional - - The vertices and edges along the path are overwritten with a different - size/width and color. - - :seealso: :meth:`highlight_vertex` :meth:`highlight_edge` - """ - for i in range(len(path)): - if i < len(path) - 1: - e = path[i].edgeto(path[i + 1]) - self.highlight_edge(e, **kwargs) - self.highlight_vertex(path[i], **kwargs) - plt.show(block=block) - - def highlight_edge(self, edge, scale=2, color="r", alpha=0.5): - """ - Highlight an edge in the graph - - :param edge: The edge to highlight - :type edge: Edge subclass - :param scale: Overwrite with a line this much bigger than the original, - defaults to 1.5 - :type scale: float, optional - :param color: Overwrite with a line in this color, defaults to 'r' - :type color: str, optional - """ - p1 = edge.v1 - p2 = edge.v2 - plt.plot( - [p1.x, p2.x], [p1.y, p2.y], color=color, linewidth=3 * scale, alpha=alpha - ) - - def highlight_vertex(self, vertex, scale=2, color="r", alpha=0.5): - """ - Highlight a vertex in the graph - - :param edge: The vertex to highlight - :type edge: Vertex subclass - :param scale: Overwrite with a line this much bigger than the original, - defaults to 1.5 - :type scale: float, optional - :param color: Overwrite with a line in this color, defaults to 'r' - :type color: str, optional - """ - if isinstance(vertex, Iterable): - for n in vertex: - if isinstance(n, str): - n = self[n] - plt.plot(n.x, n.y, "o", color=color, markersize=12 * scale, alpha=alpha) - else: - plt.plot( - vertex.x, vertex.y, "o", color=color, markersize=12 * scale, alpha=alpha - ) - - def dotfile(self, filename=None, direction=None): - """ - Export graph as a GraphViz dot file - - :param filename: filename to save graph to, defaults to None - :type filename: str, optional - - ``g.dotfile()`` creates the specified file which contains the - `GraphViz `_ code to represent the embedded graph. By default output - is to the console. - - .. note:: - - - The graph is undirected if it is a subclass of ``UGraph`` - - The graph is directed if it is a subclass of ``DGraph`` - - Use ``neato`` rather than dot to get the embedded layout - - .. note:: If ``filename`` is a file object then the file will *not* - be closed after the GraphViz model is written. - - :seealso: :func:`showgraph` - """ - - if filename is None: - f = sys.stdout - elif isinstance(filename, str): - f = open(filename, "w") - else: - f = filename - - if isinstance(self, DGraph): - print("digraph {", file=f) - else: - print("graph {", file=f) - - if direction is not None: - print(f"rankdir = {direction}", file=f) - - # add the vertices including name and position - for vertex in self: - if vertex.coord is None: - print(' "{:s}"'.format(vertex.name), file=f) - else: - print( - ' "{:s}" [pos="{:.5g},{:.5g}"]'.format( - vertex.name, vertex.coord[0], vertex.coord[1] - ), - file=f, - ) - print(file=f) - # add the edges - for e in self.edges(): - if isinstance(self, DGraph): - print(' "{:s}" -> "{:s}"'.format(e.v1.name, e.v2.name), file=f) - else: - print(' "{:s}" -- "{:s}"'.format(e.v1.name, e.v2.name), file=f) - - print("}", file=f) - - if filename is None or isinstance(filename, str): - f.close() # noqa - - def showgraph(self, **kwargs): - """ - Display graph in a browser tab - - :param kwargs: arguments passed to :meth:`dotfile` - - ``g.showgraph()`` renders and displays the graph in a browser tab. The - graph is exported in `GraphViz `_ format, rendered to - PDF using ``dot`` and then displayed in a browser tab. - - :seealso: :func:`dotfile` - """ - # create the temporary dotfile - dotfile = tempfile.TemporaryFile(mode="w") - self.dotfile(dotfile, **kwargs) - - # rewind the dot file, create PDF file in the filesystem, run dot - dotfile.seek(0) - pdffile = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) - result = subprocess.run("dot -Tpdf", shell=True, stdin=dotfile, stdout=pdffile) - - if result.returncode == 0: - # dot ran happily - # open the PDF file in browser (hopefully portable), then cleanup - webbrowser.open(f"file://{pdffile.name}") - # time.sleep(1) - # os.remove(pdffile.name) - - def iscyclic(self): - pass - - def average_degree(self): - r""" - Average degree of the graph - - :return: average degree - :rtype: float - - Average degree is :math:`2 E / N` for an undirected graph and - :math:`E / N` for a directed graph where :math:`E` is the total number of - edges and :math:`N` is the number of vertices. - - """ - if isinstance(self, DGraph): - return len(self.edges()) / self.n - elif isinstance(self, UGraph): - return 2 * len(self.edges()) / self.n - - # --------------------------------------------------------------------------- # - - # MATRIX REPRESENTATIONS - - def Laplacian(self): - """ - Laplacian matrix for the graph - - :return: Laplacian matrix - :rtype: NumPy ndarray - - ``g.Laplacian()`` is the Laplacian matrix (NxN) of the graph where N - is the number of vertices. - - .. note:: - - - Laplacian is always positive-semidefinite. - - Laplacian has at least one zero eigenvalue. - - The number of zero-valued eigenvalues is the number of connected - components in the graph. - - :seealso: :meth:`adjacency` :meth:`incidence` :meth:`degree` - """ - return self.degree() - (self.adjacency() > 0) - - def connectivity(self, vertices=None): - """ - Graph connectivity - - :return: a list with the number of edges per vertex - :rtype: list - - The average vertex connectivity is:: - - mean(g.connectivity()) - - and the minimum vertex connectivity is:: - - min(g.connectivity()) - """ - - c = [] - if vertices is None: - vertices = self - for n in vertices: - c.append(len(n._edgelist)) - return c - - def degree(self): - """ - Degree matrix of graph - - :return: degree matrix - :rtype: ndarray(N,N) - - This is a diagonal matrix where element ``[i,i]`` is the number - of edges connected to vertex id ``i``. - - :seealso: :meth:`adjacency` :meth:`incidence` :meth:`laplacian` - """ - - return np.diag(self.connectivity()) - - def adjacency(self): - """ - Adjacency matrix of graph - - :returns: adjacency matrix - :rtype: ndarray(N,N) - - The elements of the adjacency matrix ``[i,j]`` are 1 if vertex ``i`` is - connected to vertex ``j``, else 0. - - .. note:: - - - vertices are numbered in their order of creation. A vertex index - can be resolved to a vertex reference by ``graph[i]``. - - for an undirected graph the matrix is symmetric - - Eigenvalues of ``A`` are real and are known as the spectrum of the graph. - - The element ``A[i,j]`` can be considered the number of walks of length one - edge from vertex ``i`` to vertex ``j`` (either zero or one). - - If ``Ak = A ** k`` the element ``Ak[i,j]`` is the number of - walks of length ``k`` from vertex ``i`` to vertex ``j``. - - :seealso: :meth:`Laplacian` :meth:`incidence` :meth:`degree` - """ - # create a dict mapping vertex to an id - vdict = {} - for i, vert in enumerate(self): - vdict[vert] = i - - A = np.zeros((self.n, self.n)) - for vertex in self: - for n in vertex.neighbours(): - A[vdict[vertex], vdict[n]] = 1 - return A - - def incidence(self): - """ - Incidence matrix of graph - - :returns: incidence matrix - :rtype: ndarray(n,ne) - - The elements of the incidence matrix ``I[i,j]`` are 1 if vertex ``i`` is - connected to edge ``j``, else 0. - - .. note:: - - - vertices are numbered in their order of creation. A vertex index - can be resolved to a vertex reference by ``graph[i]``. - - edges are numbered in the order they appear in ``graph.edges()``. - - :seealso: :meth:`Laplacian` :meth:`adjacency` :meth:`degree` - """ - edges = self.edges() - I = np.zeros((self.n, len(edges))) - - # create a dict mapping edge to an id - edict = {} - for i, edge in enumerate(edges): - edict[edge] = i - - for i, vertex in enumerate(self): - for i, e in enumerate(vertex.edges()): - I[i, edict[e]] = 1 - - return I - - def distance(self): - """ - Distance matrix of graph - - :return: distance matrix - :rtype: ndarray(n,n) - - The elements of the distance matrix ``D[i,j]`` is the edge cost of moving - from vertex ``i`` to vertex ``j``. It is zero if the vertices are not - connected. - """ - # create a dict mapping vertex to an id - vdict = {} - for i, vert in enumerate(self): - vdict[vert] = i - - A = np.zeros((self.n, self.n)) - for v1 in self: - for v2, edge in v1.incidences(): - A[vdict[v1], vdict[v2]] = edge.cost - return A - - # GRAPH COMPONENTS - - def component(self, c): - """ - All vertices in specified graph component - - ``graph.component(c)`` is a list of all vertices in graph component ``c``. - """ - self._graphcolor() # ensure labels are uptodate - return [v for v in self if v.label == c] - - def samecomponent(self, v1, v2): - """ - Test if vertices belong to same graph component - - :param v1: vertex - :type v1: Vertex subclass - :param v2: vertex - :type v2: Vertex subclass - :return: true if vertices belong to same graph component - :rtype: bool - - Test whether vertices belong to the same component. For a: - - - directed graph this implies a path between them - - undirected graph there is not necessarily a path between them - """ - self._graphcolor() # ensure labels are uptodate - - return v1.label == v2.label - - # def remove(self, v): - # # remove edges from neighbour's edge list - # for e in v.edges(): - # next = e.next(v) - # next._edgelist.remove(e) - # next._connectivitychange = True - - # # remove references from the graph - # self._vertexlist.remove(v) - # for key, value in self._vertexdict.items(): - # if value is v: - # del self._vertexdict[key] - # break - - # v._edgelist = [] # remove all references to edges - # --------------------------------------------------------------------------- # - - def path_BFS(self, S, G, verbose=False, summary=False): - """ - Breadth-first search for path - - :param S: start vertex - :type S: Vertex subclass - :param G: goal vertex - :type G: Vertex subclass - :return: list of vertices from S to G inclusive, path length - :rtype: list of Vertex subclass, float - - Returns a list of vertices that form a path from vertex ``S`` to - vertex ``G`` if possible, otherwise return None. - - """ - if isinstance(S, str): - S = self[S] - elif not isinstance(S, Vertex): - raise TypeError("start must be Vertex subclass or string name") - if isinstance(G, str): - G = self[G] - elif not isinstance(S, Vertex): - raise TypeError("goal must be Vertex subclass or string name") - - # we use lists not sets since the order is instructive in verbose - # mode, really need ordered sets... - frontier = [S] - explored = [] - parent = {} - done = False - - while frontier: - if verbose: - print() - print("FRONTIER:", ", ".join([v.name for v in frontier])) - print("EXPLORED:", ", ".join([v.name for v in explored])) - - x = frontier.pop(0) - if verbose: - print(" expand", x.name) - - # expand the vertex - for n in x.neighbours(): - if n is G: - if verbose: - print(" goal", n.name, "reached") - parent[n] = x - done = True - break - if n not in frontier and n not in explored: - # add it to the frontier - frontier.append(n) - if verbose: - print(" add", n.name, "to the frontier") - parent[n] = x - if done: - break - explored.append(x) - if verbose: - print(" move", x.name, "to the explored list") - else: - # no path - return None - - # reconstruct the path from start to goal - x = G - path = [x] - length = 0 - - while x is not S: - p = parent[x] - length += x.edgeto(p).cost - path.insert(0, p) - x = p - - if summary or verbose: - print( - f"{len(explored)} vertices explored, {len(frontier)} remaining on the frontier" - ) - - return path, length - - def path_UCS(self, S, G, verbose=False, summary=False): - """ - Uniform cost search for path - - :param S: start vertex - :type S: Vertex subclass - :param G: goal vertex - :type G: Vertex subclass - :return: list of vertices from S to G inclusive, path length, tree - :rtype: list of Vertex subclass, float, dict - - Returns a list of vertices that form a path from vertex ``S`` to - vertex ``G`` if possible, otherwise return None. - - The search tree is returned as dict that maps a vertex to its parent. - - The heuristic is the distance metric of the graph, which defaults to - Euclidean distance. - """ - if isinstance(S, str): - S = self[S] - elif not isinstance(S, Vertex): - raise TypeError("start must be Vertex subclass or string name") - if isinstance(G, str): - G = self[G] - elif not isinstance(S, Vertex): - raise TypeError("goal must be Vertex subclass or string name") - - frontier = [S] - explored = [] - parent = {} - f = {S: 0} # evaluation function - - while frontier: - if verbose: - print() - print( - "FRONTIER:", ", ".join([f"{v.name}({f[v]:.0f})" for v in frontier]) - ) - print("EXPLORED:", ", ".join([v.name for v in explored])) - - i = np.argmin([f[n] for n in frontier]) # minimum f in frontier - x = frontier.pop(i) - if verbose: - print(" expand", x.name) - if x is G: - break - # expand the vertex - for n, e in x.incidences(): - fnew = f[x] + e.cost - if n not in frontier and n not in explored: - # add it to the frontier - parent[n] = x - f[n] = fnew - frontier.append(n) - if verbose: - print(" add", n.name, "to the frontier") - - elif n in frontier: - # neighbour is already in the frontier - # cost of path via x is lower that previous, reparent it - if fnew < f[n]: - if verbose: - print( - f" reparent {n.name}: cost {fnew} via {x.name} is less than cost {f[n]} via {parent[n].name}, change parent from {parent[n].name} to {x.name} " - ) - f[n] = fnew - parent[n] = x - - explored.append(x) - if verbose: - print(" move", x.name, "to the explored list") - else: - # no path - return None - - # reconstruct the path from start to goal - x = G - path = [x] - length = 0 - - while x is not S: - p = parent[x] - length += p.edgeto(x).cost - path.insert(0, p) - x = p - - parent_names = {} - for v, p in parent.items(): - parent_names[v.name] = p.name - - if summary or verbose: - print( - f"{len(explored)} vertices explored, {len(frontier)} remaining on the frontier" - ) - - return path, length, parent_names - - def path_Astar(self, S, G, verbose=False, summary=False): - """ - A* search for path - - :param S: start vertex - :type S: Vertex subclass - :param G: goal vertex - :type G: Vertex subclass - :return: list of vertices from S to G inclusive, path length, tree - :rtype: list of Vertex subclass, float, dict - - Returns a list of vertices that form a path from vertex ``S`` to - vertex ``G`` if possible, otherwise return None. - - The search tree is returned as dict that maps a vertex to its parent. - - The heuristic is the distance metric of the graph, which defaults to - Euclidean distance. - - :seealso: :meth:`heuristic` - """ - if isinstance(S, str): - S = self[S] - elif not isinstance(S, Vertex): - raise TypeError("start must be Vertex subclass or string name") - if isinstance(G, str): - G = self[G] - elif not isinstance(S, Vertex): - raise TypeError("goal must be Vertex subclass or string name") - - frontier = [S] - explored = [] - parent = {} - g = {S: 0} # cost to come - f = {S: 0} # evaluation function - - while frontier: - if verbose: - print() - print( - "FRONTIER:", ", ".join([f"{v.name}({f[v]:.0f})" for v in frontier]) - ) - print("EXPLORED:", ", ".join([v.name for v in explored])) - - i = np.argmin([f[n] for n in frontier]) # minimum f in frontier - x = frontier.pop(i) - if verbose: - print(" expand", x.name) - if x is G: - break - # expand the vertex - for n, e in x.incidences(): - if n not in frontier and n not in explored: - # add it to the frontier - frontier.append(n) - parent[n] = x - g[n] = g[x] + e.cost # update cost to come - f[n] = g[n] + n.heuristic_distance(G) # heuristic - if verbose: - print(" add", n.name, "to the frontier") - elif n in frontier: - # neighbour is already in the frontier - gnew = g[x] + e.cost - if gnew < g[n]: - # cost of path via x is lower that previous, reparent it - if verbose: - print( - f" reparent {n.name}: cost {gnew} via {x.name} is less than cost {g[n]} via {parent[n].name}, change parent from {parent[n].name} to {x.name} " - ) - g[n] = gnew - f[n] = g[n] + n.heuristic_distance(G) # heuristic - - parent[n] = x # reparent - - explored.append(x) - if verbose: - print(" move", x.name, "to the explored list") - - else: - # no path - return None - - # reconstruct the path from start to goal - x = G - path = [x] - length = 0 - - while x is not S: - p = parent[x] - length += p.edgeto(x).cost - path.insert(0, p) - x = p - - parent_names = {} - for v, p in parent.items(): - parent_names[v.name] = p.name - - if summary or verbose: - print( - f"{len(explored)} vertices explored, {len(frontier)} remaining on the frontier" - ) - - return path, length, parent_names - - -# -------------------------------------------------------------------------- # - - -class UGraph(PGraph): - """ - Class for undirected graphs - - .. inheritance-diagram:: UGraph - """ - - def add_vertex(self, coord=None, name=None): - """ - Add vertex to undirected graph - - :param coord: coordinate for an embedded graph, defaults to None - :type coord: array-like, optional - :param name: vertex name, defaults to "#i" - :type name: str, optional - :return: new vertex - :rtype: UVertex - - - ``g.add_vertex()`` creates a new vertex with optional ``coord`` and - ``name``. - - ``g.add_vertex(v)`` takes an instance or subclass of UVertex and adds - it to the graph - """ - if isinstance(coord, UVertex): - vertex = coord - else: - vertex = UVertex(coord) - super().add_vertex(vertex, name=name) - return vertex - - @classmethod - def vertex_copy(self, vertex): - return DVertex(coord=vertex.coord, name=vertex.name) - - def _graphcolor(self): - """ - Color the graph - - Performs a depth-first labeling operation, assigning the ``label`` - attribute of every vertex with a sequential integer starting from 0. - - This method checks the ``_connectivitychange`` attribute of all vertices - and if any are True it will perform the coloring operation. This flag - is set True by any operation that adds or removes a vertex or edge. - - :seealso: :meth:`nc` - """ - if self._connectivitychange or any([n._connectivitychange for n in self]): - - # color the graph - - # clear all the labels - for vertex in self: - vertex.label = None - vertex._connectivitychange = False - - lastlabel = None - for label in range(self.n): - assignment = False - for v in self: - # find first vertex with no label - if v.label is None: - # do BFS - q = [v] # initialize frontier - while len(q) > 0: - v = q.pop() # expand v - v.label = label - for n in v.neighbours(): - if n.label is None: - q.append(n) - lastlabel = label - assignment = True - break - if not assignment: - break - - self._ncomponents = lastlabel + 1 - - -class DGraph(PGraph): - """ - Class for directed graphs - - .. inheritance-diagram:: DGraph - """ - - def add_vertex(self, coord=None, name=None): - """ - Add vertex to directed graph - - :param coord: coordinate for an embedded graph, defaults to None - :type coord: array-like, optional - :param name: vertex name, defaults to "#i" - :type name: str, optional - :return: new vertex - :rtype: DVertex - - - ``g.add_vertex()`` creates a new vertex with optional ``coord`` and - ``name``. - - ``g.add_vertex(v)`` takes an instance or subclass of DVertex and adds - it to the graph - """ - if isinstance(coord, Vertex): - vertex = coord - else: - vertex = DVertex(coord=coord, name=name) - super().add_vertex(vertex, name=name) - return vertex - - @classmethod - def vertex_copy(self, vertex): - return DVertex(coord=vertex.coord, name=vertex.name) - - def _graphcolor(self): - """ - Color the graph - - Performs a depth-first labeling operation, assigning the ``label`` - attribute of every vertex with a sequential integer starting from 0. - - This method checks the ``_connectivitychange`` attribute of all vertices - and if any are True it will perform the coloring operation. This flag - is set True by any operation that adds or removes a vertex or edge. - - :seealso: :meth:`nc` - """ - if self._connectivitychange or any([n._connectivitychange for n in self]): - - # color the graph - - # clear all the labels - for vertex in self: - vertex.label = None - vertex._connectivitychange = False - - # initial labeling pass - merge = {} - nextlabel = 1 - for v in self: - if v.label is None: - # no label, try to inherit one from a neighbour - for n in v.neighbours(): - if n.label is not None: - # neighbour has a label - v.label = n.label - break - - if v.label is None: - # still not labeled, assign a new label - v.label = nextlabel - nextlabel += 1 - - # now look for clashes - for n in v.neighbours(): - if n.label is None: - # neighbour has no label, give it this one - n.label = v.label - elif v.label != n.label: - # label clash, note it for merging - merge[n.label] = v.label - - # merge labels and find unique labels - unique = set() - for v in self: - while v.label in merge: - v.label = merge[v.label] - unique.add(v.label) - - final = {u: i for i, u in enumerate(unique)} - for v in self: - v.label = final[v.label] - - self._ncomponents = len(unique) - - -# ========================================================================== # - - -class Edge: - """ - Edge class - - Is used to represent directed directed and undirected edges. - - Each edge has: - - ``cost`` cost of traversing this edge, required for planning methods - - ``data`` reference to arbitrary data associated with the edge - - ``v1`` first vertex, start vertex for a directed edge - - ``v2`` second vertex, end vertex for a directed edge - - .. note:: - - - An undirected graph is created by having a single edge object in the - edgelist of _each_ vertex. - - This class can be inherited to provide user objects with graph capability. - - Inheritance is an alternative to providing arbitrary user data. - - An Edge points to a pair of vertices. At ``connect`` time the vertices - get references back to the Edge object. - - ``graph.add_edge(v1, v2)`` calls ``v1.connect(v2)`` - """ - - def __init__(self, v1=None, v2=None, cost=None, data=None): - """ - Create an edge object - - :param v1: start of the edge, defaults to None - :type v1: Vertex subclass, optional - :param v2: end of the edge, defaults to None - :type v2: Vertex subclass, optional - :param cost: edge cost, defaults to None - :type cost: any, optional - :param data: edge data, defaults to None - :type data: any, optional - - Creates an edge but does not connect it to the vertices or add it to the - graph. - - If vertices are given, and have associated coordinates, the edge cost - will be computed according to the distance measure associated with the - graph. - - ``data`` is a way of associating any object with the edge, its value - can be found as the ``.data`` attribute of the edge. An alternative - approach is to subclass the ``Edge`` class. - - .. note:: To compute edge cost from the vertices, the vertices must have - been added to the graph. - - :seealso: :meth:`Edge.connect` :meth:`Vertex.connect` - """ - self.v1 = v1 - self.v2 = v2 - - self.data = data - - # try to compute edge cost as metric distance if not given - if cost is not None: - self.cost = cost - elif not (v1 is None or v1.coord is None or v2 is None or v2.coord is None): - self.cost = v1._graph.metric(v1.coord - v2.coord) - else: - self.cost = None - - def __repr__(self): - return str(self) - - def __str__(self): - - s = f"Edge{{{self.v1} -- {self.v2}, cost={self.cost:.4g}}}" - if self.data is not None: - s += f" data={self.data}" - return s - - def connect(self, v1, v2): - """ - Add edge to the graph - - :param v1: start of the edge - :type v1: Vertex subclass - :param v2: end of the edge - :type v2: Vertex subclass - - The edge is added to the graph and connects vertices ``v1`` and ``v2``. - - .. note:: The vertices must already be added to the graph. - """ - - if v1._graph is None: - raise ValueError("vertex v1 does not belong to a graph") - if v2._graph is None: - raise ValueError("vertex v2 does not belong to a graph") - if not v1._graph is v2._graph: - raise ValueError("vertices must belong to the same graph") - - # connect edge to its vertices - self.v1 = v1 - self.v2 = v2 - - # tell the vertices to add edge to their edgelists as appropriate for - # DGraph or UGraph - v1.connect(v2, edge=self) - - def next(self, vertex): - """ - Return other end of an edge - - :param vertex: one vertex on the edge - :type vertex: Vertex subclass - :raises ValueError: ``vertex`` is not on the edge - :return: the other vertex on the edge - :rtype: Vertex subclass - - ``e.next(v1)`` is the vertex at the other end of edge ``e``, ie. the - vertex that is not ``v1``. - """ - - if self.v1 is vertex: - return self.v2 - elif self.v2 is vertex: - return self.v1 - else: - raise ValueError("shouldnt happen") - - def vertices(self): - raise DeprecationWarning("use endpoints instead") - - @property - def endpoints(self): - return [self.v1, self.v2] - - # def remove(self): - # """ - # Remove edge from graph - - # ``e.remove()`` removes ``e`` from the graph, but does not delete the - # edge object. - # """ - # # remove this edge from the edge list of both end vertices - # if self in self.v1._edgelist: - # self.v1._edgelist.remove(self) - # if self in self.v2._edgelist: - # self.v2._edgelist.remove(self) - - # # indicate that connectivity has changed - # self.v1._connectivitychange = True - # self.v2._connectivitychange = True - - # # remove references to the vertices - # self.v1 = None - # self.v2 = None - - -# ========================================================================== # - - -class Vertex: - """ - Superclass for vertices of directed and non-directed graphs. - - Each vertex has: - - ``name`` - - ``label`` an int indicating which graph component contains it - - ``_edgelist`` a list of edge objects that connect this vertex to others - - ``coord`` the coordinate in an embedded graph (optional) - """ - - def __init__(self, coord=None, name=None): - self._edgelist = [] - if coord is None: - self.coord = None - else: - self.coord = np.r_[coord] - self.name = name - self.label = None - self._connectivitychange = True - self._edgelist = [] - self._graph = None # reference to owning graph - # print('Vertex init', type(self)) - - def __str__(self): - return f"[{self.name:s}]" - - def __repr__(self): - if self.coord is None: - coord = "?" - else: - coord = ", ".join([f"{x:.4g}" for x in self.coord]) - return f"{self.__class__.__name__}[{self.name:s}, coord=({coord})]" - - def copy(self, cls=None): - if cls is not None: - return cls.vertex_copy(self) - else: - return self.__class__(coord=self.coord, name=self.name) - - def adjacent(self): - """ - Adjacent vertices - - ``v.adjacent()`` is a list of vertices adjacent to this vertex. - - :note: For a directed graph the neighbours are those on edges leaving this vertex - - :seealso: :meth:`neighbours` :meth:`neighbors` - """ - return [e.next(self) for e in self._edgelist] - - def neighbours(self): - """ - Neighbours of a vertex - - ``v.neighbours()`` is a list of neighbours of this vertex. - - .. note:: - - For a directed graph the neighbours are those on edges leaving this vertex - - British English synonym for :meth:`adjacent` - - :seealso: :meth:`adjacent` :meth:`neighbours` - """ - return self.adjacent() - - def neighbors(self): - """ - Neighbors of a vertex - - ``v.neighbors()`` is a list of neighbors of this vertex. - - .. note:: - - For a directed graph the neighbors are those on edges leaving this vertex - - US English synonym for :meth:`adjacent` - - :seealso: :meth:`adjacent` :meth:`neighbours` - """ - return self.adjacent() - - def isneighbour(self, vertex): - """ - Test if vertex is a neigbour - - :param vertex: vertex reference - :type vertex: Vertex subclass - :return: true if a neighbour - :rtype: bool - - For a directed graph this is true only if the edge is from ``self`` to - ``vertex``. - """ - return vertex in [e.next(self) for e in self._edgelist] - - def incidences(self): - """ - Neighbours and edges of a vertex - - ``v.incidences()`` is a generator that returns a list of incidences, - tuples of (vertex, edge) for all neighbours of the vertex ``v``. - - .. note:: For a directed graph the edges are those leaving this vertex - """ - return [(e.next(self), e) for e in self._edgelist] - - def connect(self, dest, edge=None, cost=None, data=None): - """ - Connect two vertices with an edge - - :param dest: The vertex to connect to - :type dest: ``Vertex`` subclass - :param edge: Use this as the edge object, otherwise a new ``Edge`` - object is created from the vertices being connected, - and the ``cost`` and ``edge`` parameters, defaults to None - :type edge: ``Edge`` subclass, optional - :param cost: the cost to traverse this edge, defaults to None - :type cost: float, optional - :param data: reference to arbitrary data associated with the edge, - defaults to None - :type data: Any, optional - :raises TypeError: vertex types are different subclasses - :return: the edge connecting the vertices - :rtype: Edge - - ``v1.connect(v2)`` connects vertex ``v1`` to vertex ``v2``. - - .. note:: - - - If the vertices subclass ``UVertex`` the edge is undirected, and if - they subclass ``DVertex`` the edge is directed. - - Vertices must both be of the same ``Vertex`` subclass - - :seealso: :meth:`Edge` - """ - - if not dest.__class__.__bases__[0] is self.__class__.__bases__[0]: - raise TypeError("must connect vertices of same type") - elif isinstance(edge, Edge): - e = edge - else: - e = Edge(self, dest, cost=cost, data=data) - - self._graph._edgelist.add(e) - self._graph._connectivitychange = True - self._connectivitychange = True - - return e - - def edgeto(self, dest): - """ - Get edge connecting vertex to specific neighbour - - :param dest: a neigbouring vertex - :type dest: ``Vertex`` subclass - :raises ValueError: ``dest`` is not a neighbour - :return: the edge from this vertex to ``dest`` - :rtype: Edge - - .. note:: - - - For a directed graph ``dest`` must be at the arrow end of the edge - """ - for n, e in self.incidences(): - if n is dest: - return e - raise ValueError("dest is not a neighbour") - - def edges(self): - """ - All outgoing edges of vertex - - :return: List of all edges leaving this vertex - :rtype: list of Edge - - .. note:: - - - For a directed graph the edges are those leaving this vertex - - For a non-directed graph the edges are those leaving or entering - this vertex - """ - return self._edgelist - - def heuristic_distance(self, v2): - return self._graph.heuristic(self.coord - v2.coord) - - def distance(self, coord): - """ - Distance from vertex to point - - :param coord: coordinates of the point - :type coord: ndarray(n) or Vertex - :return: distance - :rtype: float - - Distance is computed according to the graph's metric. - - :seealso: :meth:`metric` - """ - if isinstance(coord, Vertex): - coord = coord.coord - return self._graph.metric(self.coord - coord) - - @property - def degree(self): - """ - Degree of vertex - - :return: degree of the vertex - :rtype: int - - Returns the number of edges connected to the vertex. - - .. note:: For a ``DGraph`` only outgoing edges are considered. - - :seealso: :meth:`edges` - """ - return len(self.edges()) - - @property - def x(self): - """ - The x-coordinate of an embedded vertex - - :return: The x-coordinate - :rtype: float - """ - return self.coord[0] - - @property - def y(self): - """ - The y-coordinate of an embedded vertex - - :return: The y-coordinate - :rtype: float - """ - return self.coord[1] - - @property - def z(self): - """ - The z-coordinate of an embedded vertex - - :return: The z-coordinate - :rtype: float - """ - return self.coord[2] - - def closest(self): - return self._graph.closest(self.coord) - - -class UVertex(Vertex): - """ - Vertex subclass for undirected graphs - - This class can be inherited to provide user objects with graph capability. - - - .. inheritance-diagram:: UVertex - - """ - - def connect(self, other, **kwargs): - - if isinstance(other, Vertex): - e = super().connect(other, **kwargs) - elif isinstance(other, Edge): - e = super().connect(edge=other) - else: - raise TypeError("bad argument") - - # e = super().connect(other, **kwargs) - - self._edgelist.append(e) - other._edgelist.append(e) - self._graph._edgelist.add(e) - - return e - - -class DVertex(Vertex): - """ - Vertex subclass for directed graphs - - This class can be inherited to provide user objects with graph capability. - - .. inheritance-diagram:: DVertex - - """ - - def connect(self, other, **kwargs): - if isinstance(other, Vertex): - e = super().connect(other, **kwargs) - elif isinstance(other, Edge): - e = super().connect(edge=other) - else: - raise TypeError("bad argument") - - self._edgelist.append(e) - return e - - def remove(self): - self._edgelist = None # remove all references to edges - - -if __name__ == "__main__": - - g = UGraph() - - for i in range(10): - g.add_vertex() - - g.add_edge(g[0], g[1]) - - print(g) diff --git a/build/lib/pgraph/__init__.py b/build/lib/pgraph/__init__.py deleted file mode 100644 index 011d41c7..00000000 --- a/build/lib/pgraph/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from pgraph.PGraph import UGraph, DGraph, UVertex, DVertex, Edge