@@ -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