From c05c2265f4c9b7d03d7389dae9e57e9ab02cb421 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Thu, 3 Sep 2026 20:29:19 +0200 Subject: [PATCH] Add dim keyword arg to enforce a fixed coordinate length per graph _BaseGraph.__init__(dim=...) declares that every embedded vertex in this graph must have a coord of exactly that length; add_vertex() checks it and raises ValueError on mismatch. Vertices added with no coord at all are unaffected regardless of dim -- there's nothing to check. Defaults to None (today's fully unconstrained behaviour). This gives real, validated meaning to what MVTB's BundleAdjust.py has always been trying to express with `pgraph.UGraph(6)` -- a comment right above that call says "initialize the graph, nodes have 6D coordinates" -- but the removed `arg` positional parameter never actually did anything with that 6. It was dead code from the start; `dim` is the real feature that call was reaching for, now as an explicit, named, and impossible-to-confuse-with-metric keyword arg. Verified: valid/invalid dim values, coord-length mismatches, and unconstrained (dim=None) graphs, including the coord=None passthrough case. New test_dim. Sphinx runblock demonstrates the happy path only, matching this file's existing convention of not featuring deliberate failures in executed examples (the ValueError case is documented via :raises: and prose instead). Co-Authored-By: Claude Sonnet 5 --- src/pgraph/PGraph.py | 35 ++++++++++++++++++++++++++++++++++- tests/test_graph.py | 23 +++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) 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()