Skip to content

Commit 78ffdb7

Browse files
petercorkeclaude
andcommitted
feat: Graph.within, Graph.near and Graph.subgraph
within(box) lists the vertices inside an n-dimensional box given as a 2 x n array of lower and upper corner, near(coord, radius) those within a distance of a point by the graph's metric with their distances, nearest first, and subgraph(vertices) is the graph induced by a set of vertices, a new graph of the same kind in which every edge with an end outside is dropped. subgraph reuses the flat description that pickling uses, so classes, names, coordinates, costs, user attributes, metric and the order of vertices and edges are kept, parallel edges and loops included, and the result shares nothing with the original. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent 849c456 commit 78ffdb7

3 files changed

Lines changed: 486 additions & 0 deletions

File tree

‎docs/source/analysis.rst‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,20 @@ Structure and connectivity
4949
Geometry and consistency
5050
^^^^^^^^^^^^^^^^^^^^^^^^^
5151

52+
For a graph whose vertices have coordinates,
53+
:meth:`~pgraph.PGraph.Graph.closest` finds the nearest vertex to a point,
54+
:meth:`~pgraph.PGraph.Graph.near` the vertices within a distance of a point and
55+
:meth:`~pgraph.PGraph.Graph.within` the vertices inside a box. Passing the
56+
result of the last to :meth:`~pgraph.PGraph.Graph.subgraph` gives the part of
57+
the graph in a region, without the edges that cross its border.
58+
5259
.. autosummary::
5360
:nosignatures:
5461

5562
~pgraph.PGraph.Graph.closest
63+
~pgraph.PGraph.Graph.near
64+
~pgraph.PGraph.Graph.within
65+
~pgraph.PGraph.Graph.subgraph
5666
~pgraph.PGraph.Graph.validate
5767

5868
The matrices of a graph (:meth:`~pgraph.PGraph.Graph.adjacency`,
@@ -154,6 +164,9 @@ Details
154164
.. automethod:: pgraph.PGraph.Graph.connectivity
155165
.. automethod:: pgraph.PGraph.Graph.average_degree
156166
.. automethod:: pgraph.PGraph.Graph.closest
167+
.. automethod:: pgraph.PGraph.Graph.near
168+
.. automethod:: pgraph.PGraph.Graph.within
169+
.. automethod:: pgraph.PGraph.Graph.subgraph
157170
.. automethod:: pgraph.PGraph.Graph.validate
158171

159172
.. autoproperty:: pgraph.PGraph.Vertex.name

‎src/pgraph/PGraph.py‎

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,70 @@ def copy(self) -> Graph:
890890
"""
891891
return copy.deepcopy(self)
892892

893+
def subgraph(self, vertices: Iterable[Vertex | str]) -> Graph:
894+
"""
895+
Subgraph induced by a set of vertices
896+
897+
:param vertices: vertices of this graph, or their names
898+
:type vertices: iterable of Vertex subclass or str
899+
:raises ValueError: a vertex is not in this graph
900+
:raises KeyError: a name is not that of a vertex in this graph
901+
:raises TypeError: an item is neither a vertex nor a string
902+
:return: a new graph of the same kind
903+
:rtype: Graph
904+
905+
The new graph has the given vertices and every edge of this graph that
906+
has *both* its end vertices among them. An edge that has one end
907+
outside is removed, so the edges that cross the border of a region
908+
are dropped. Parallel edges and loops are kept.
909+
910+
Like :meth:`copy` the new graph is independent of this one: its
911+
vertices and edges are new objects that keep their class, name,
912+
coordinate, cost and any attributes that were added, and the graph
913+
keeps its metric, heuristic and vertex and edge classes. They are in
914+
the order of this graph, however ``vertices`` was ordered. Names are
915+
kept, so the vertex ``g[name]`` has its counterpart ``h[name]``.
916+
917+
.. runblock:: pycon
918+
919+
>>> from pgraph import UGraph
920+
>>> g = UGraph()
921+
>>> v = [g.add_vertex(coord=[i, 0], name=f"v{i}") for i in range(4)]
922+
>>> for i in range(3):
923+
... g.add_edge(v[i], v[i + 1])
924+
>>> h = g.subgraph([v[1], v[2]])
925+
>>> print(h)
926+
>>> h.edges()
927+
928+
It is a single pass over the graph. The vertices might be a component,
929+
the vertices that satisfy a test, or those in a region of an embedded
930+
graph from :meth:`within`.
931+
932+
:seealso: :meth:`copy` :meth:`within` :meth:`component`
933+
"""
934+
wanted = set()
935+
for v in vertices:
936+
v = self._resolve_vertex(v, "vertex")
937+
if v._graph is not self:
938+
raise ValueError(f"vertex {v.name!r} is not in this graph")
939+
wanted.add(id(v))
940+
941+
state = self.__getstate__()
942+
flat = state["_flat"]
943+
keep = [i for i, v in enumerate(self._vertexlist) if id(v) in wanted]
944+
renumber = {old: new for new, old in enumerate(keep)}
945+
flat["vertices"] = [flat["vertices"][i] for i in keep]
946+
flat["edges"] = [
947+
(cls, renumber[i], renumber[j], attrs)
948+
for cls, i, j, attrs in flat["edges"]
949+
if i in renumber and j in renumber
950+
]
951+
952+
h = type(self).__new__(type(self))
953+
h.__setstate__(copy.deepcopy(state))
954+
h._connectivitychange = True # the components are those of the new graph
955+
return h
956+
893957
# pickling and copying
894958

895959
#: the graph's own containers, which are saved as a flat description
@@ -1844,6 +1908,98 @@ def closest(self, coord: ArrayLike) -> tuple[Vertex | None, float]:
18441908

18451909
return min_which, min_dist
18461910

1911+
def within(self, box: ArrayLike) -> list[Vertex]:
1912+
"""
1913+
Vertices inside a box
1914+
1915+
:param box: lower and upper corners of the box
1916+
:type box: ndarray(2,n)
1917+
:raises ValueError: ``box`` is not 2 x n, its upper corner is below its
1918+
lower corner in some dimension, or the coordinate of a vertex does
1919+
not have n elements
1920+
:return: the vertices inside the box, in the order of the graph
1921+
:rtype: list of Vertex subclass
1922+
1923+
The first row of ``box`` is the lower corner, the second row the upper
1924+
corner, so in two dimensions it is ``[[xmin, ymin], [xmax, ymax]]``.
1925+
The box is closed: a vertex on its boundary is inside. Vertices
1926+
without a coordinate are skipped, as they are by :meth:`closest`.
1927+
1928+
It is a single pass over the vertices. Together with
1929+
:meth:`subgraph` it selects the part of an embedded graph in a region
1930+
1931+
.. runblock:: pycon
1932+
1933+
>>> from pgraph import UGraph
1934+
>>> g = UGraph()
1935+
>>> for i in range(5):
1936+
... g.add_vertex(coord=[i, i], name=f"v{i}")
1937+
>>> g.within([[1, 1], [3, 3]])
1938+
>>> g.subgraph(g.within([[1, 1], [3, 3]]))
1939+
1940+
:seealso: :meth:`near` :meth:`closest` :meth:`subgraph`
1941+
"""
1942+
box = np.asarray(box, dtype=float)
1943+
if box.ndim != 2 or box.shape[0] != 2:
1944+
raise ValueError(f"box must be 2 x n, not {box.shape}")
1945+
lower, upper = box
1946+
if np.any(upper < lower):
1947+
raise ValueError("the upper corner of the box is below the lower corner")
1948+
1949+
found = []
1950+
for vertex in self:
1951+
if vertex.coord is None:
1952+
continue
1953+
coord = np.asarray(vertex.coord, dtype=float)
1954+
if coord.shape != lower.shape:
1955+
raise ValueError(
1956+
f"vertex {vertex.name!r} has a coordinate of {coord.size} "
1957+
f"elements, the box has {lower.size}"
1958+
)
1959+
if np.all(coord >= lower) and np.all(coord <= upper):
1960+
found.append(vertex)
1961+
return found
1962+
1963+
def near(self, coord: ArrayLike, radius: float) -> list[tuple[Vertex, float]]:
1964+
"""
1965+
Vertices within a distance of a point
1966+
1967+
:param coord: coordinates of a point
1968+
:type coord: ndarray(n)
1969+
:param radius: greatest distance, inclusive
1970+
:raises ValueError: ``radius`` is negative
1971+
:return: the vertices within ``radius`` of the point with their
1972+
distances, nearest first
1973+
:rtype: list of (Vertex subclass, float)
1974+
1975+
Distance is computed according to the graph's metric, as it is by
1976+
:meth:`closest`, of which this is the generalization to every vertex
1977+
within a radius. Vertices without a coordinate are skipped, and
1978+
vertices at equal distance are in the order of the graph.
1979+
1980+
.. runblock:: pycon
1981+
1982+
>>> from pgraph import UGraph
1983+
>>> g = UGraph()
1984+
>>> for i in range(5):
1985+
... g.add_vertex(coord=[i, 0], name=f"v{i}")
1986+
>>> [(v.name, d) for v, d in g.near([2.2, 0], 1.5)]
1987+
1988+
:seealso: :meth:`closest` :meth:`within` :meth:`metric`
1989+
"""
1990+
if radius < 0:
1991+
raise ValueError(f"radius must not be negative, got {radius!r}")
1992+
1993+
found = []
1994+
for vertex in self:
1995+
if vertex.coord is None:
1996+
continue
1997+
d = float(self.metric(vertex.coord - coord))
1998+
if d <= radius:
1999+
found.append((vertex, d))
2000+
found.sort(key=lambda pair: pair[1])
2001+
return found
2002+
18472003
def edges(self) -> list[Edge]:
18482004
"""
18492005
All edges in the graph, in the order they were added

0 commit comments

Comments
 (0)