diff --git a/src/pgraph/PGraph.py b/src/pgraph/PGraph.py index 65f060ca..f92d00de 100644 --- a/src/pgraph/PGraph.py +++ b/src/pgraph/PGraph.py @@ -28,6 +28,7 @@ def __init__( metric: Callable[[NDArray], float] | str | None = None, heuristic: Callable[[NDArray], float] | str | None = None, verbose: bool = False, + dim: int | None = None, ): """ Abstract base class for graphs @@ -39,17 +40,25 @@ def __init__( :type heuristic: callable or str, optional :param verbose: print diagnostic information as vertices/edges are added, defaults to False + :param dim: required length of every vertex's ``coord``, defaults to + None (unconstrained -- vertices may have coordinates of any + length, or none at all) + :type dim: int, optional + :raises ValueError: ``dim`` is given but is not a positive integer This is the common base class of :class:`UGraph` and :class:`DGraph` and should not be instantiated directly. - :seealso: :class:`UGraph` :class:`DGraph` + :seealso: :class:`UGraph` :class:`DGraph` :meth:`add_vertex` """ + if dim is not None and dim <= 0: + raise ValueError(f"dim must be a positive integer, got {dim!r}") # we use a list and a dict, the list respects the order of adding self._vertexlist: list[BaseVertex] = [] self._vertexdict: dict[str, BaseVertex] = {} self._edgelist: set[Edge] = set() self._verbose = verbose + self._dim = dim self._ncomponents = 0 self._connectivitychange = False if metric is None: @@ -249,6 +258,8 @@ def add_vertex( :param name: name of vertex, defaults to "#i" :type name: str, optional :raises TypeError: ``coord`` is a ``BaseVertex`` of the wrong kind + :raises ValueError: the graph was constructed with ``dim``, and + ``coord`` is given but its length doesn't match :return: the added vertex :rtype: BaseVertex subclass @@ -276,6 +287,18 @@ def add_vertex( >>> v2 = g.add_vertex(UVertex(coord=[1,1], name='v2')) >>> print(v2.name) + If the graph was constructed with a required ``dim`` (see + :meth:`_BaseGraph.__init__`), every embedded vertex must have a + coordinate of exactly that length -- adding one of the wrong length + raises ``ValueError``: + + .. runblock:: pycon + + >>> from pgraph import UGraph + >>> g = UGraph(dim=6) + >>> v1 = g.add_vertex(coord=[0, 0, 0, 0, 0, 0], name='pose1') + >>> print(v1) + :seealso: :meth:`vertex_copy` """ if isinstance(coord, self._vertex_cls): @@ -288,6 +311,16 @@ def add_vertex( else: vertex = self._vertex_cls(coord, name=name) + if ( + self._dim is not None + and vertex.coord is not None + and len(vertex.coord) != self._dim + ): + raise ValueError( + f"vertex coord has length {len(vertex.coord)}, " + f"but this graph requires dim={self._dim}" + ) + if name is None: name = vertex.name if name is None: diff --git a/tests/test_graph.py b/tests/test_graph.py index 94502b15..6e06f6fe 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -358,6 +358,29 @@ def test_add_vertex(self): self.assertTrue(v in g) self.assertTrue(v._graph, g) + def test_dim(self): + + # unconstrained by default: any length, or no coord at all + g = UGraph() + g.add_vertex(coord=[1, 2]) + g.add_vertex(coord=[1, 2, 3, 4]) + g.add_vertex() + self.assertEqual(g.n, 3) + + # dim enforces every embedded vertex has exactly that length + g = UGraph(dim=6) + g.add_vertex(coord=[0, 0, 0, 0, 0, 0], name='pose1') + g.add_vertex(name='untyped') # no coord: not checked + with self.assertRaises(ValueError): + g.add_vertex(coord=[1, 2, 3], name='bad') + self.assertEqual(g.n, 2) + + # dim must be a positive integer + with self.assertRaises(ValueError): + UGraph(dim=0) + with self.assertRaises(ValueError): + UGraph(dim=-1) + def test_properties(self): g = UGraph()