From 6f15849e2f10659bcc6679e2dc4fd16ea9070999 Mon Sep 17 00:00:00 2001 From: Stefano Grava Date: Fri, 7 Aug 2026 09:38:35 +0200 Subject: [PATCH 01/13] coords and weights to follow networkx --- qoolqit/graphs/base_graph.py | 54 ++++++++-------------------- qoolqit/graphs/data_graph.py | 25 +++++-------- tests/test_graphs/test_base_graph.py | 1 + 3 files changed, 23 insertions(+), 57 deletions(-) diff --git a/qoolqit/graphs/base_graph.py b/qoolqit/graphs/base_graph.py index e4e4ad8d7..7c50c7af4 100644 --- a/qoolqit/graphs/base_graph.py +++ b/qoolqit/graphs/base_graph.py @@ -26,26 +26,6 @@ class BaseGraph(nx.Graph): distances, and checking if the graph is unit-disk. """ - def __init__(self, edges: Iterable = []) -> None: - """ - Default constructor for the BaseGraph. - - Arguments: - edges: set of edge tuples (i, j) - """ - if edges and not isinstance(edges, Iterable): - raise TypeError("Input is not a valid edge list.") - - super().__init__() - self.add_edges_from(edges) - self._coords = {i: None for i in self.nodes} - self._reset_dicts() - - def _reset_dicts(self) -> None: - """Reset the default weight dictionaries.""" - self._node_weights = {n: None for n in self.nodes} - self._edge_weights = {e: None for e in self.sorted_edges} - @classmethod def from_nodes(cls, nodes: Iterable) -> BaseGraph: """Construct a base graph from a set of nodes. @@ -55,8 +35,6 @@ def from_nodes(cls, nodes: Iterable) -> BaseGraph: """ graph = cls() graph.add_nodes_from(nodes) - graph._coords = {i: None for i in graph.nodes} - graph._reset_dicts() return graph @classmethod @@ -67,15 +45,11 @@ def from_coordinates(cls, coords: list | dict) -> BaseGraph: coords: list or dictionary of coordinate pairs. """ if isinstance(coords, list): - nodes = list(range(len(coords))) - coords_dict = {i: pos for i, pos in enumerate(coords)} + coords_tuple = ((i, {"pos": pos}) for i, pos in enumerate(coords)) elif isinstance(coords, dict): - nodes = list(coords.keys()) - coords_dict = coords - graph = cls.from_nodes(nodes) - graph._coords = coords_dict - graph._reset_dicts() - return graph + coords_tuple = ((key, {"pos": pos}) for key, pos in coords.items()) + + return cls.from_nodes(coords_tuple) @classmethod def from_nx(cls, g: nx.Graph) -> BaseGraph: @@ -176,8 +150,8 @@ def has_coords(self) -> bool: Requires all nodes to have coordinates. """ - is_any_coord_none = any(value is None for value in self._coords.values()) - return not (is_any_coord_none or len(self._coords) == 0) + missing_pos = [node for node, data in self.nodes(data=True) if "pos" not in data] + return len(missing_pos) == 0 @property def has_edges(self) -> bool: @@ -190,7 +164,8 @@ def has_node_weights(self) -> bool: Requires all nodes to have a weight. """ - return not ((None in self._node_weights.values()) or len(self._node_weights) == 0) + missing_weights = [n for n, data in self.nodes(data=True) if "weight" not in data] + return len(missing_weights) == 0 @property def has_edge_weights(self) -> bool: @@ -198,12 +173,13 @@ def has_edge_weights(self) -> bool: Requires all edges to have a weight. """ - return not ((None in self._edge_weights.values()) or len(self._edge_weights) == 0) + missing_weights = [(u, v) for u, v, data in self.edges(data=True) if "weight" not in data] + return len(missing_weights) == 0 @property def coords(self) -> dict: """Return the dictionary of node coordinates.""" - return self._coords + return nx.get_node_attributes(self, "pos", default=None) @coords.setter def coords(self, coords: list | dict) -> None: @@ -221,9 +197,7 @@ def coords(self, coords: list | dict) -> None: "Set of nodes in the given dictionary does not match the graph nodes." ) coords_dict = coords - self._coords = coords_dict - - # methods + nx.set_node_attributes(self, coords_dict, "pos") def distances(self, edge_list: Iterable | None = None) -> dict: """Returns a dictionary of distances for a given set of edges. @@ -358,9 +332,9 @@ def rescale_coords( if (len(args) > 0) or (scaling is None and spacing is None): raise TypeError(msg) if scaling is None and spacing is not None: - self._coords = space_coords(self._coords, spacing) + self.coords = space_coords(self.coords, spacing) elif spacing is None and scaling is not None: - self._coords = scale_coords(self._coords, scaling) + self.coords = scale_coords(self.coords, scaling) else: raise TypeError(msg) else: diff --git a/qoolqit/graphs/data_graph.py b/qoolqit/graphs/data_graph.py index e5492e08a..407f68f79 100644 --- a/qoolqit/graphs/data_graph.py +++ b/qoolqit/graphs/data_graph.py @@ -43,8 +43,7 @@ def line(cls, n: int, spacing: float = 1.0) -> DataGraph: coords = [(i * spacing, 0.0) for i in range(n)] graph = cls.from_coordinates(coords) edges = [(i, i + 1) for i in range(0, n - 1)] - graph.add_edges_from(edges) - graph._reset_dicts() + graph.add_edges_from(edges, weight=1.0) return graph @classmethod @@ -70,8 +69,7 @@ def circle( ] edges = [(i, i + 1) for i in range(n - 1)] + [(n - 1, 0)] graph = cls.from_coordinates(coords) - graph.add_edges_from(edges) - graph._reset_dicts() + graph.add_edges_from(edges, weight=1.0) return graph @classmethod @@ -86,7 +84,6 @@ def random_er(cls, n: int, p: float, seed: int | None = None) -> DataGraph: base_graph = nx.erdos_renyi_graph(n, p, seed) graph = DataGraph.from_nodes(list(base_graph.nodes)) graph.add_edges_from(base_graph.edges) - graph._reset_dicts() return graph @classmethod @@ -112,7 +109,6 @@ def triangular( graph = cls.from_coordinates(final_pos) graph.add_edges_from(G.edges) - graph._reset_dicts() return graph @classmethod @@ -138,7 +134,6 @@ def hexagonal( graph = cls.from_coordinates(final_pos) graph.add_edges_from(G.edges) - graph._reset_dicts() return graph @classmethod @@ -199,7 +194,6 @@ def heavy_hexagonal( graph = cls.from_coordinates(final_coords) graph.add_edges_from(final_edges) - graph._reset_dicts() return graph @classmethod @@ -223,7 +217,6 @@ def square( graph = DataGraph.from_coordinates(final_coords) graph.add_edges_from(G.edges) - graph._reset_dicts() return graph @classmethod @@ -251,7 +244,6 @@ def random_ud( graph = cls.from_coordinates(coords) edges = graph.ud_edges(radius) graph.add_edges_from(edges) - graph._reset_dicts() return graph @classmethod @@ -430,7 +422,6 @@ def from_pyg( # Re-initialize QoolQit internal dicts for all nodes/edges graph._coords = {n: None for n in graph.nodes} - graph._reset_dicts() # pos → _coords (stored as list [x, y] by to_networkx) for node, node_data in nx_graph.nodes(data=True): @@ -450,7 +441,7 @@ def from_pyg( v = int(data.edge_index[1, idx].item()) key = (min(u, v), max(u, v)) if key not in seen: - graph._edge_weights[key] = edge_tensor[idx].item() + graph.edge_weights[key] = edge_tensor[idx].item() seen.add(key) return graph @@ -536,7 +527,7 @@ def to_pyg( # Export _coords → pos if self.has_coords: - positions = [self._coords[n] for n in sorted(self.nodes())] + positions = [self.coords[n] for n in sorted(self.nodes())] data.pos = torch.tensor(positions, dtype=torch.float64) # Export _node_weights → node_weights_attr @@ -623,7 +614,7 @@ def _validate_weights_attr( @property def node_weights(self) -> dict: """Return the dictionary of node weights.""" - return self._node_weights + return nx.get_node_attributes(self, "weight", default=None) @node_weights.setter def node_weights(self, weights: list | dict) -> None: @@ -643,12 +634,12 @@ def node_weights(self, weights: list | dict) -> None: "Set of nodes in the given dictionary does not match the graph nodes." ) weights_dict = weights - self._node_weights = weights_dict + nx.set_node_attributes(self, weights_dict, "weight") @property def edge_weights(self) -> dict: """Return the dictionary of edge weights.""" - return self._edge_weights + return nx.get_edge_attributes(self, "weight", default=1.0) @edge_weights.setter def edge_weights(self, weights: list | dict) -> None: @@ -668,7 +659,7 @@ def edge_weights(self, weights: list | dict) -> None: "Set of edges in the given dictionary does not match the graph ordered edges." ) weights_dict = weights - self._edge_weights = weights_dict + nx.set_edge_attributes(self, weights_dict, "weight") def set_ud_edges(self, radius: float) -> None: """Reset the set of edges to be equal to the set of unit-disk edges.""" diff --git a/tests/test_graphs/test_base_graph.py b/tests/test_graphs/test_base_graph.py index b4e8a0ed7..e6aea63df 100644 --- a/tests/test_graphs/test_base_graph.py +++ b/tests/test_graphs/test_base_graph.py @@ -76,6 +76,7 @@ def test_basegraph_interactions(n_nodes: int) -> None: rng = np.random.default_rng(0) coords_array = rng.uniform(-1, 1, size=(n_nodes, 2)) graph = BaseGraph.from_coordinates([c for c in coords_array]) + assert graph.has_coords expected_interactions = { (i, j): np.linalg.norm(coords_array[i] - coords_array[j]) ** (-6) From bd027d34b6bf577d8247abe8d75116b044ccc900 Mon Sep 17 00:00:00 2001 From: Stefano Grava Date: Fri, 7 Aug 2026 17:52:06 +0200 Subject: [PATCH 02/13] deleteng useles methods --- qoolqit/graphs/base_graph.py | 208 +++++++++++++-------------- qoolqit/graphs/data_graph.py | 4 +- tests/test_graphs/test_base_graph.py | 2 - 3 files changed, 101 insertions(+), 113 deletions(-) diff --git a/qoolqit/graphs/base_graph.py b/qoolqit/graphs/base_graph.py index 7c50c7af4..1a12b410a 100644 --- a/qoolqit/graphs/base_graph.py +++ b/qoolqit/graphs/base_graph.py @@ -30,8 +30,11 @@ class BaseGraph(nx.Graph): def from_nodes(cls, nodes: Iterable) -> BaseGraph: """Construct a base graph from a set of nodes. - Arguments: - nodes: set of nodes. + Args: + nodes: Iterable container. + Can be a container of nodes (list, dict, set, etc.) or + a container of (node, attribute dict) tuples. + Node attributes are updated using the attribute dict. """ graph = cls() graph.add_nodes_from(nodes) @@ -41,7 +44,11 @@ def from_nodes(cls, nodes: Iterable) -> BaseGraph: def from_coordinates(cls, coords: list | dict) -> BaseGraph: """Construct a base graph from a set of coordinates. - Arguments: + From a list of coordinates, nodes are labelled with their index. + From a dictionary, nodes are labelled with their keys. + Each node is added to the graph with its position as a node attribute `pos`. + + Args: coords: list or dictionary of coordinate pairs. """ if isinstance(coords, list): @@ -51,86 +58,6 @@ def from_coordinates(cls, coords: list | dict) -> BaseGraph: return cls.from_nodes(coords_tuple) - @classmethod - def from_nx(cls, g: nx.Graph) -> BaseGraph: - """Convert a NetworkX Graph object into a QoolQit graph instance. - - The input `networkx.Graph` graph must be defined only with the following allowed - - Node attributes: - pos (tuple): represents the node 2D position. Must be a list/tuple of real numbers. - weight: represents the node weight. Must be a real number. - Edge attributes: - weight: represents the edge weight. Must be a real number. - - Returns an instance of the class with following attributes: - - _node_weights : dict[node, float or None] - - _edge_weights : dict[(u,v), float or None] - - _coords : dict[node, (float,float) or None] - """ - if not isinstance(g, nx.Graph): - raise TypeError("Input must be a networkx.Graph instance.") - - g = nx.convert_node_labels_to_integers(g) - num_nodes = len(g.nodes) - num_edges = len(g.edges) - - # validate node attributes - for name, data in g.nodes.data(): - unexpected_keys = set(data) - {"weight", "pos"} - if unexpected_keys: - raise ValueError(f"{unexpected_keys} not allowed in node attributes.") - - node_pos = nx.get_node_attributes(g, "pos") - if node_pos: - if len(node_pos) != num_nodes: - raise ValueError("Node attribute `pos` must be defined for all nodes") - for name, pos in node_pos.items(): - is_2D = isinstance(pos, (tuple, list)) & (len(pos) == 2) - is_real = all(isinstance(p, (float, int)) for p in pos) - if not (is_2D & is_real): - raise TypeError( - f"In node {name} the `pos` attribute must be a 2D tuple/list" - f" of real numbers, got {pos} instead." - ) - node_weights = nx.get_node_attributes(g, "weight") - if node_weights: - if len(node_weights) != num_nodes: - raise ValueError("Node attribute `weight` must be defined for all nodes") - for name, weight in node_weights.items(): - if not isinstance(weight, (float, int)): - raise TypeError( - f"In node {name} the `weight` attribute must be a real number, " - f"got {type(weight)} instead." - "" - ) - - # validate edge attributes - for u, v, data in g.edges.data(): - unexpected_keys = set(data) - {"weight"} - if unexpected_keys: - raise ValueError(f"{unexpected_keys} not allowed in edge attributes.") - edge_weights = nx.get_edge_attributes(g, "weight") - if edge_weights: - if len(edge_weights) != num_edges: - raise ValueError("Edge attribute `weight` must be defined for all edges") - for name, weight in edge_weights.items(): - if not isinstance(weight, (float, int)): - raise TypeError( - f"In edge {name}, the attribute `weight` must be a real number, " - f"got {type(weight)} instead." - ) - - # build the instance of the graph - graph = cls() - graph.add_nodes_from(g.nodes) - graph.add_edges_from(g.edges) - graph._node_weights = nx.get_node_attributes(g, "weight", default=None) - graph._coords = nx.get_node_attributes(g, "pos", default=None) - graph._edge_weights = nx.get_edge_attributes(g, "weight", default=None) - - return graph - @property def sorted_edges(self) -> set: """Returns the set of edges (u, v) such that (u < v).""" @@ -146,46 +73,29 @@ def all_node_pairs(self) -> set: @property def has_coords(self) -> bool: - """Check if the graph has coordinates. - - Requires all nodes to have coordinates. - """ - missing_pos = [node for node, data in self.nodes(data=True) if "pos" not in data] - return len(missing_pos) == 0 - - @property - def has_edges(self) -> bool: - """Check if the graph has edges.""" - return len(self.edges) > 0 + """Check if the graph has coordinates on all nodes.""" + return all(pos for _, pos in self.nodes(data="pos")) @property def has_node_weights(self) -> bool: - """Check if the graph has node weights. - - Requires all nodes to have a weight. - """ - missing_weights = [n for n, data in self.nodes(data=True) if "weight" not in data] - return len(missing_weights) == 0 + """Check if the graph has node weights on all nodes.""" + return all(w for _, w in self.nodes(data="weight")) @property def has_edge_weights(self) -> bool: - """Check if the graph has edge weights. - - Requires all edges to have a weight. - """ - missing_weights = [(u, v) for u, v, data in self.edges(data=True) if "weight" not in data] - return len(missing_weights) == 0 + """Check if the graph has edge weights on all edges.""" + return all(w for _, _, w in self.edges(data="weight")) @property def coords(self) -> dict: - """Return the dictionary of node coordinates.""" - return nx.get_node_attributes(self, "pos", default=None) + """Return a dictionary of node coordinates.""" + return dict(self.nodes(data="pos")) @coords.setter def coords(self, coords: list | dict) -> None: """Set the dictionary of node coordinates. - Arguments: + Args: coords: list or dictionary of coordinate pairs. """ if isinstance(coords, list): @@ -349,6 +259,86 @@ def set_ud_edges(self, radius: float) -> None: self.remove_edges_from(list(self.edges)) self.add_edges_from(self.ud_edges(radius)) + @classmethod + def from_nx(cls, g: nx.Graph) -> BaseGraph: + """Convert a NetworkX Graph object into a QoolQit graph instance. + + The input `networkx.Graph` graph must be defined only with the following allowed + + Node attributes: + pos (tuple): represents the node 2D position. Must be a list/tuple of real numbers. + weight: represents the node weight. Must be a real number. + Edge attributes: + weight: represents the edge weight. Must be a real number. + + Returns an instance of the class with following attributes: + - _node_weights : dict[node, float or None] + - _edge_weights : dict[(u,v), float or None] + - _coords : dict[node, (float,float) or None] + """ + if not isinstance(g, nx.Graph): + raise TypeError("Input must be a networkx.Graph instance.") + + g = nx.convert_node_labels_to_integers(g) + num_nodes = len(g.nodes) + num_edges = len(g.edges) + + # validate node attributes + for name, data in g.nodes.data(): + unexpected_keys = set(data) - {"weight", "pos"} + if unexpected_keys: + raise ValueError(f"{unexpected_keys} not allowed in node attributes.") + + node_pos = nx.get_node_attributes(g, "pos") + if node_pos: + if len(node_pos) != num_nodes: + raise ValueError("Node attribute `pos` must be defined for all nodes") + for name, pos in node_pos.items(): + is_2D = isinstance(pos, (tuple, list)) & (len(pos) == 2) + is_real = all(isinstance(p, (float, int)) for p in pos) + if not (is_2D & is_real): + raise TypeError( + f"In node {name} the `pos` attribute must be a 2D tuple/list" + f" of real numbers, got {pos} instead." + ) + node_weights = nx.get_node_attributes(g, "weight") + if node_weights: + if len(node_weights) != num_nodes: + raise ValueError("Node attribute `weight` must be defined for all nodes") + for name, weight in node_weights.items(): + if not isinstance(weight, (float, int)): + raise TypeError( + f"In node {name} the `weight` attribute must be a real number, " + f"got {type(weight)} instead." + "" + ) + + # validate edge attributes + for u, v, data in g.edges.data(): + unexpected_keys = set(data) - {"weight"} + if unexpected_keys: + raise ValueError(f"{unexpected_keys} not allowed in edge attributes.") + edge_weights = nx.get_edge_attributes(g, "weight") + if edge_weights: + if len(edge_weights) != num_edges: + raise ValueError("Edge attribute `weight` must be defined for all edges") + for name, weight in edge_weights.items(): + if not isinstance(weight, (float, int)): + raise TypeError( + f"In edge {name}, the attribute `weight` must be a real number, " + f"got {type(weight)} instead." + ) + + # build the instance of the graph + graph = cls() + graph.add_nodes_from(g.nodes) + graph.add_edges_from(g.edges) + graph._node_weights = nx.get_node_attributes(g, "weight", default=None) + graph._coords = nx.get_node_attributes(g, "pos", default=None) + graph._edge_weights = nx.get_edge_attributes(g, "weight", default=None) + + return graph + def draw(self, ax: Axes | None = None, **kwargs: Any) -> None: """Draw the graph. diff --git a/qoolqit/graphs/data_graph.py b/qoolqit/graphs/data_graph.py index 407f68f79..0cd12317d 100644 --- a/qoolqit/graphs/data_graph.py +++ b/qoolqit/graphs/data_graph.py @@ -614,7 +614,7 @@ def _validate_weights_attr( @property def node_weights(self) -> dict: """Return the dictionary of node weights.""" - return nx.get_node_attributes(self, "weight", default=None) + return dict(self.nodes(data="weight")) @node_weights.setter def node_weights(self, weights: list | dict) -> None: @@ -639,7 +639,7 @@ def node_weights(self, weights: list | dict) -> None: @property def edge_weights(self) -> dict: """Return the dictionary of edge weights.""" - return nx.get_edge_attributes(self, "weight", default=1.0) + return dict(self.edges(data="weight")) @edge_weights.setter def edge_weights(self, weights: list | dict) -> None: diff --git a/tests/test_graphs/test_base_graph.py b/tests/test_graphs/test_base_graph.py index e6aea63df..478a1a78d 100644 --- a/tests/test_graphs/test_base_graph.py +++ b/tests/test_graphs/test_base_graph.py @@ -28,7 +28,6 @@ def test_basegraph_init(n_nodes: int) -> None: assert len(graph.sorted_edges) == n_edges assert len(graph.sorted_edges) <= max_n_edges assert graph.sorted_edges == set(edge_list) - assert graph.has_edges assert not graph.has_coords with pytest.raises(AttributeError): @@ -105,7 +104,6 @@ def test_basegraph_constructors(n_nodes: int) -> None: for graph in [graph1, graph2]: assert len(graph.edges) == 0 assert len(graph.sorted_edges) == 0 - assert not graph.has_edges assert not graph1.has_coords assert graph2.has_coords From c09f0f369d94fefa8728a3d5be632407e1209d27 Mon Sep 17 00:00:00 2001 From: Stefano Grava Date: Mon, 10 Aug 2026 15:16:39 +0200 Subject: [PATCH 03/13] clean coords and weights attrs --- qoolqit/graphs/base_graph.py | 72 ++++++++++++++++----- qoolqit/graphs/data_graph.py | 94 +++++----------------------- tests/test_graphs/test_base_graph.py | 12 ++-- tests/test_graphs/test_data_graph.py | 27 ++++---- 4 files changed, 93 insertions(+), 112 deletions(-) diff --git a/qoolqit/graphs/base_graph.py b/qoolqit/graphs/base_graph.py index 1a12b410a..6b9cd71e4 100644 --- a/qoolqit/graphs/base_graph.py +++ b/qoolqit/graphs/base_graph.py @@ -74,17 +74,67 @@ def all_node_pairs(self) -> set: @property def has_coords(self) -> bool: """Check if the graph has coordinates on all nodes.""" - return all(pos for _, pos in self.nodes(data="pos")) + return all("pos" in n for _, n in self.nodes(data=True)) @property def has_node_weights(self) -> bool: """Check if the graph has node weights on all nodes.""" - return all(w for _, w in self.nodes(data="weight")) + return all("weight" in n for _, n in self.nodes(data=True)) @property def has_edge_weights(self) -> bool: """Check if the graph has edge weights on all edges.""" - return all(w for _, _, w in self.edges(data="weight")) + return all("weight" in e for _, _, e in self.edges(data=True)) + + @property + def node_weights(self) -> dict: + """Return the dictionary of node weights.""" + return dict(self.nodes(data="weight")) + + @node_weights.setter + def node_weights(self, weights: list | dict) -> None: + """Set the dictionary of node weights. + + Arguments: + weights: list or dictionary of weights. + """ + if isinstance(weights, list): + if len(weights) != self.number_of_nodes(): + raise ValueError("Size of the weights list does not match the number of nodes.") + weights_dict = {i: w for i, w in zip(self.nodes, weights)} + elif isinstance(weights, dict): + nodes = set(weights.keys()) + if set(self.nodes) != nodes: + raise ValueError( + "Set of nodes in the given dictionary does not match the graph nodes." + ) + weights_dict = weights + nx.set_node_attributes(self, weights_dict, "weight") + + @property + def edge_weights(self) -> dict: + """Return the dictionary of edge weights.""" + return {(u, v): w for u, v, w in self.edges(data="weight")} + + @edge_weights.setter + def edge_weights(self, weights: list | dict) -> None: + """Set the dictionary of edge weights. + + Arguments: + weights: list or dictionary of weights. + """ + if isinstance(weights, list): + if len(weights) != self.number_of_edges(): + raise ValueError("Size of the weights list does not match the number of nodes.") + weights_dict = {i: w for i, w in zip(self.sorted_edges, weights)} + elif isinstance(weights, dict): + edges = set(weights.keys()) + if set(self.sorted_edges) != edges: + raise ValueError( + "Set of edges in the given dictionary does not match the graph ordered edges." + ) + weights_dict = weights + nx.set_edge_attributes(self, weights_dict, "weight") @property def coords(self) -> dict: @@ -272,9 +322,9 @@ def from_nx(cls, g: nx.Graph) -> BaseGraph: weight: represents the edge weight. Must be a real number. Returns an instance of the class with following attributes: - - _node_weights : dict[node, float or None] - - _edge_weights : dict[(u,v), float or None] - - _coords : dict[node, (float,float) or None] + - node_weights : dict[node, float or None] + - edge_weights : dict[(u,v), float or None] + - coords : dict[node, (float,float) or None] """ if not isinstance(g, nx.Graph): raise TypeError("Input must be a networkx.Graph instance.") @@ -329,15 +379,7 @@ def from_nx(cls, g: nx.Graph) -> BaseGraph: f"got {type(weight)} instead." ) - # build the instance of the graph - graph = cls() - graph.add_nodes_from(g.nodes) - graph.add_edges_from(g.edges) - graph._node_weights = nx.get_node_attributes(g, "weight", default=None) - graph._coords = nx.get_node_attributes(g, "pos", default=None) - graph._edge_weights = nx.get_edge_attributes(g, "weight", default=None) - - return graph + return cls(g) def draw(self, ax: Axes | None = None, **kwargs: Any) -> None: """Draw the graph. diff --git a/qoolqit/graphs/data_graph.py b/qoolqit/graphs/data_graph.py index 0cd12317d..088bd3821 100644 --- a/qoolqit/graphs/data_graph.py +++ b/qoolqit/graphs/data_graph.py @@ -331,13 +331,13 @@ def from_pyg( **Default attributes copied (if present on** ``data`` **):** - - Node: ``x``, ``pos`` (``pos`` is also stored in ``_coords``) + - Node: ``x``, ``pos`` (``pos`` is also stored in ``coords``) - Edge: ``edge_attr`` - Graph: ``y`` Use ``node_attrs``, ``edge_attrs``, ``graph_attrs`` for extras. - **QoolQit weights** (``_node_weights``, ``_edge_weights``) are not + **QoolQit weights** (``node_weights``, ``edge_weights``) are not populated automatically — use the explicit parameters: - ``node_weights_attr``: real-valued tensor of shape ``(N,)`` or @@ -357,7 +357,7 @@ def from_pyg( edge_weights_attr: Data attribute to use as edge weights. Returns: - DataGraph with ``_coords``, ``_node_weights``, ``_edge_weights`` + DataGraph with ``coords``, ``node_weights``, ``edge_weights`` populated where applicable. Raises: @@ -416,24 +416,14 @@ def from_pyg( ) # Build the DataGraph: edges carry their data, nodes carry their data - graph = cls(nx_graph.edges(data=True)) - graph.add_nodes_from(nx_graph.nodes(data=True)) - graph.graph = nx_graph.graph + graph = cls(nx_graph) - # Re-initialize QoolQit internal dicts for all nodes/edges - graph._coords = {n: None for n in graph.nodes} - - # pos → _coords (stored as list [x, y] by to_networkx) - for node, node_data in nx_graph.nodes(data=True): - if "pos" in node_data: - graph._coords[node] = tuple(node_data["pos"]) # type: ignore[assignment] - - # node_weights_attr → _node_weights + # node_weights_attr → node_weights if node_tensor is not None: for i in range(data.num_nodes): - graph._node_weights[i] = node_tensor[i].item() + graph.nodes[i]["weight"] = node_tensor[i].item() - # edge_weights_attr → _edge_weights + # edge_weights_attr → edge_weights if edge_tensor is not None: seen: set = set() for idx in range(data.edge_index.shape[1]): @@ -441,7 +431,7 @@ def from_pyg( v = int(data.edge_index[1, idx].item()) key = (min(u, v), max(u, v)) if key not in seen: - graph.edge_weights[key] = edge_tensor[idx].item() + graph.edges[key]["weight"] = edge_tensor[idx].item() seen.add(key) return graph @@ -467,10 +457,10 @@ def to_pyg( **QoolQit internal dicts exported when populated:** - - ``_coords`` → ``data.pos`` (float64, shape ``(N, 2)``) - - ``_node_weights`` → ``data.`` (float64, shape + - ``coords`` → ``data.pos`` (float64, shape ``(N, 2)``) + - ``node_weights`` → ``data.`` (float64, shape ``(N,)``). Defaults to ``"weight"``. - - ``_edge_weights`` → ``data.`` (float64, shape + - ``edge_weights`` → ``data.`` (float64, shape ``(2*E,)``). Defaults to ``"edge_weight"``. Arguments: @@ -525,23 +515,23 @@ def to_pyg( data = from_networkx(filtered_graph) - # Export _coords → pos + # Export coords → pos if self.has_coords: positions = [self.coords[n] for n in sorted(self.nodes())] data.pos = torch.tensor(positions, dtype=torch.float64) - # Export _node_weights → node_weights_attr + # Export node_weights → node_weights_attr if self.has_node_weights: - weights = [self._node_weights[n] for n in sorted(self.nodes())] + weights = [self.node_weights[n] for n in sorted(self.nodes())] setattr(data, node_weights_attr, torch.tensor(weights, dtype=torch.float64)) - # Export _edge_weights → edge_weights_attr (one value per directed edge in edge_index) + # Export edge_weights → edge_weights_attr (one value per directed edge in edge_index) if self.has_edge_weights: edge_weights: list[float] = [] for i in range(data.edge_index.shape[1]): u, v = int(data.edge_index[0, i].item()), int(data.edge_index[1, i].item()) edge_key = (min(u, v), max(u, v)) - edge_weights.append(float(self._edge_weights[edge_key])) # type: ignore[arg-type] + edge_weights.append(float(self.edge_weights[edge_key])) setattr(data, edge_weights_attr, torch.tensor(edge_weights, dtype=torch.float64)) return data @@ -611,57 +601,7 @@ def _validate_weights_attr( return weights - @property - def node_weights(self) -> dict: - """Return the dictionary of node weights.""" - return dict(self.nodes(data="weight")) - - @node_weights.setter - def node_weights(self, weights: list | dict) -> None: - """Set the dictionary of node weights. - - Arguments: - weights: list or dictionary of weights. - """ - if isinstance(weights, list): - if len(weights) != self.number_of_nodes(): - raise ValueError("Size of the weights list does not match the number of nodes.") - weights_dict = {i: w for i, w in zip(self.nodes, weights)} - elif isinstance(weights, dict): - nodes = set(weights.keys()) - if set(self.nodes) != nodes: - raise ValueError( - "Set of nodes in the given dictionary does not match the graph nodes." - ) - weights_dict = weights - nx.set_node_attributes(self, weights_dict, "weight") - - @property - def edge_weights(self) -> dict: - """Return the dictionary of edge weights.""" - return dict(self.edges(data="weight")) - - @edge_weights.setter - def edge_weights(self, weights: list | dict) -> None: - """Set the dictionary of edge weights. - - Arguments: - weights: list or dictionary of weights. - """ - if isinstance(weights, list): - if len(weights) != self.number_of_edges(): - raise ValueError("Size of the weights list does not match the number of nodes.") - weights_dict = {i: w for i, w in zip(self.sorted_edges, weights)} - elif isinstance(weights, dict): - edges = set(weights.keys()) - if set(self.sorted_edges) != edges: - raise ValueError( - "Set of edges in the given dictionary does not match the graph ordered edges." - ) - weights_dict = weights - nx.set_edge_attributes(self, weights_dict, "weight") - def set_ud_edges(self, radius: float) -> None: """Reset the set of edges to be equal to the set of unit-disk edges.""" super().set_ud_edges(radius=radius) - self._edge_weights = {e: None for e in self.sorted_edges} + self.edge_weights = {e: None for e in self.sorted_edges} diff --git a/tests/test_graphs/test_base_graph.py b/tests/test_graphs/test_base_graph.py index 478a1a78d..5ec0fa09e 100644 --- a/tests/test_graphs/test_base_graph.py +++ b/tests/test_graphs/test_base_graph.py @@ -187,9 +187,9 @@ def test_from_nx() -> None: assert set(g.edges) == set([(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]) # Check whether the coords exist and are all None - assert all(v is None for v in g._coords.values()) - assert all(v is None for v in g._node_weights.values()) - assert all(v is None for v in g._edge_weights.values()) + assert all(v is None for v in g.coords.values()) + assert all(v is None for v in g.node_weights.values()) + assert all(v is None for v in g.edge_weights.values()) def test_from_nx_with_weights_and_pos() -> None: @@ -209,10 +209,10 @@ def test_from_nx_with_weights_and_pos() -> None: assert set(g.nodes) == {0, 1, 2} assert set(g.edges) == {(0, 1), (1, 2), (0, 2)} - assert g._node_weights == {0: 1.0, 1: 2.0, 2: 3.0} - assert g._edge_weights == {(0, 1): 0.1, (1, 2): 0.2, (0, 2): 0.3} + assert g.node_weights == {0: 1.0, 1: 2.0, 2: 3.0} + assert g.edge_weights == {(0, 1): 0.1, (1, 2): 0.2, (0, 2): 0.3} - assert g._coords == { + assert g.coords == { 0: (0.0, 0.0), 1: (1.0, 0.0), 2: (0.5, 1.0), diff --git a/tests/test_graphs/test_data_graph.py b/tests/test_graphs/test_data_graph.py index 1da3b404c..a20fe8377 100644 --- a/tests/test_graphs/test_data_graph.py +++ b/tests/test_graphs/test_data_graph.py @@ -33,7 +33,6 @@ def test_datagraph_unit_disk(n_nodes: int, graph_type: str) -> None: assert len(graph.node_weights) == graph.number_of_nodes() assert len(graph.edge_weights) == graph.number_of_edges() assert not graph.has_node_weights - assert not graph.has_edge_weights assert graph.is_ud_graph() # Save a radius value where the graph is unit-disk @@ -104,7 +103,7 @@ def test_datagraph_from_matrix(n_nodes: int) -> None: graph = DataGraph.from_matrix(data2) - assert not graph.has_node_weights + assert graph.has_node_weights assert graph.has_edge_weights for edge in random_edges_removal: @@ -424,9 +423,9 @@ def test_from_pyg_only_edges() -> None: g = DataGraph.from_pyg(data) assert set(g.nodes) == {0, 1, 2} - assert all(v is None for v in g._node_weights.values()) - assert all(v is None for v in g._coords.values()) - assert all(v is None for v in g._edge_weights.values()) + assert all(v is None for v in g.node_weights.values()) + assert all(v is None for v in g.coords.values()) + assert all(v is None for v in g.edge_weights.values()) def test_from_pyg_with_qoolqit_attrs() -> None: @@ -440,9 +439,9 @@ def test_from_pyg_with_qoolqit_attrs() -> None: g = DataGraph.from_pyg(data, node_weights_attr="weight", edge_weights_attr="edge_weight") - assert g._node_weights == {0: 1.0, 1: 2.0, 2: 3.0} - assert g._edge_weights == {(0, 1): 0.1, (1, 2): 0.2, (0, 2): 0.3} - assert g._coords == {0: (0.0, 0.0), 1: (1.0, 0.0), 2: (0.5, 1.0)} + assert g.node_weights == {0: 1.0, 1: 2.0, 2: 3.0} + assert g.edge_weights == {(0, 1): 0.1, (1, 2): 0.2, (0, 2): 0.3} + assert g.coords == {0: [0.0, 0.0], 1: [1.0, 0.0], 2: [0.5, 1.0]} def test_from_pyg_with_pyg_attrs() -> None: @@ -481,7 +480,7 @@ def test_from_pyg_no_auto_weight_detection() -> None: g = DataGraph.from_pyg(data) - assert all(v is None for v in g._node_weights.values()) + assert all(v is None for v in g.node_weights.values()) def test_from_pyg_with_node_weights_attr() -> None: @@ -492,7 +491,7 @@ def test_from_pyg_with_node_weights_attr() -> None: g = DataGraph.from_pyg(data, node_weights_attr="x") - assert g._node_weights == {0: 1.0, 1: 2.0, 2: 3.0} + assert g.node_weights == {0: 1.0, 1: 2.0, 2: 3.0} def test_from_pyg_with_node_weights_attr_1d() -> None: @@ -503,7 +502,7 @@ def test_from_pyg_with_node_weights_attr_1d() -> None: g = DataGraph.from_pyg(data, node_weights_attr="my_weights") - assert g._node_weights == {0: 10.0, 1: 20.0, 2: 30.0} + assert g.node_weights == {0: 10.0, 1: 20.0, 2: 30.0} def test_from_pyg_with_edge_weights_attr() -> None: @@ -514,7 +513,7 @@ def test_from_pyg_with_edge_weights_attr() -> None: g = DataGraph.from_pyg(data, edge_weights_attr="edge_attr") - assert g._edge_weights == {(0, 1): 0.1, (1, 2): 0.2, (0, 2): 0.3} + assert g.edge_weights == {(0, 1): 0.1, (1, 2): 0.2, (0, 2): 0.3} def test_from_pyg_with_edge_weights_attr_1d() -> None: @@ -525,7 +524,7 @@ def test_from_pyg_with_edge_weights_attr_1d() -> None: g = DataGraph.from_pyg(data, edge_weights_attr="my_edge_w") - assert g._edge_weights == {(0, 1): 0.5, (1, 2): 0.6, (0, 2): 0.7} + assert g.edge_weights == {(0, 1): 0.5, (1, 2): 0.6, (0, 2): 0.7} def test_from_pyg_weights_attr_wrong_shape() -> None: @@ -690,7 +689,7 @@ def test_from_pyg_to_pyg_roundtrip_custom_weights_attr() -> None: g = DataGraph.from_pyg(data, node_weights_attr="my_node_w", edge_weights_attr="my_edge_w") - assert g._node_weights == {0: 1.0, 1: 2.0, 2: 3.0} + assert g.node_weights == {0: 1.0, 1: 2.0, 2: 3.0} roundtrip_data = g.to_pyg(node_weights_attr="my_node_w", edge_weights_attr="my_edge_w") From d229479c32d82dc14be85c06828035bc69067884 Mon Sep 17 00:00:00 2001 From: Stefano Grava Date: Mon, 10 Aug 2026 17:12:50 +0200 Subject: [PATCH 04/13] update node weights form matrix --- qoolqit/graphs/data_graph.py | 7 ++++--- tests/test_graphs/test_data_graph.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/qoolqit/graphs/data_graph.py b/qoolqit/graphs/data_graph.py index 088bd3821..6a16e1c2b 100644 --- a/qoolqit/graphs/data_graph.py +++ b/qoolqit/graphs/data_graph.py @@ -268,8 +268,8 @@ def from_matrix(cls, data: npt.NDArray[np.float64]) -> DataGraph: diag = np.diag(data) n_nodes = len(diag) - if np.allclose(diag, np.zeros(n_nodes), rtol=0.0, atol=nonzero_tol): - node_weights = {i: None for i in range(n_nodes)} + if np.allclose(diag, np.zeros_like(diag), rtol=0.0, atol=nonzero_tol): + node_weights = None else: node_weights = {i: diag[i].item() for i in range(n_nodes)} @@ -283,7 +283,8 @@ def from_matrix(cls, data: npt.NDArray[np.float64]) -> DataGraph: graph = cls.from_nodes(range(n_nodes)) graph.add_edges_from(edge_list) - graph.node_weights = node_weights + if node_weights is not None: + graph.node_weights = node_weights graph.edge_weights = edge_weights return graph diff --git a/tests/test_graphs/test_data_graph.py b/tests/test_graphs/test_data_graph.py index a20fe8377..f3b1b4a22 100644 --- a/tests/test_graphs/test_data_graph.py +++ b/tests/test_graphs/test_data_graph.py @@ -103,8 +103,8 @@ def test_datagraph_from_matrix(n_nodes: int) -> None: graph = DataGraph.from_matrix(data2) - assert graph.has_node_weights - assert graph.has_edge_weights + assert not graph.has_node_weights + assert graph.has_edge_weights # still has edge weights since edges with are removed for edge in random_edges_removal: assert edge not in graph.sorted_edges From 037917df4d5de260d1847aba75fb9d099acaff06 Mon Sep 17 00:00:00 2001 From: Stefano Grava Date: Mon, 10 Aug 2026 17:38:05 +0200 Subject: [PATCH 05/13] fix has_attr --- qoolqit/graphs/base_graph.py | 22 ++++++++++++++-------- qoolqit/graphs/data_graph.py | 9 --------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/qoolqit/graphs/base_graph.py b/qoolqit/graphs/base_graph.py index 6b9cd71e4..b4dc80cd9 100644 --- a/qoolqit/graphs/base_graph.py +++ b/qoolqit/graphs/base_graph.py @@ -74,17 +74,23 @@ def all_node_pairs(self) -> set: @property def has_coords(self) -> bool: """Check if the graph has coordinates on all nodes.""" - return all("pos" in n for _, n in self.nodes(data=True)) + return self.number_of_nodes() > 0 and all( + pos is not None for _, pos in self.nodes(data="pos") + ) @property def has_node_weights(self) -> bool: """Check if the graph has node weights on all nodes.""" - return all("weight" in n for _, n in self.nodes(data=True)) + return self.number_of_nodes() > 0 and all( + w is not None for _, w in self.nodes(data="weight") + ) @property def has_edge_weights(self) -> bool: """Check if the graph has edge weights on all edges.""" - return all("weight" in e for _, _, e in self.edges(data=True)) + return self.number_of_edges() > 0 and all( + w is not None for _, _, w in self.edges(data="weight") + ) @property def node_weights(self) -> dict: @@ -165,7 +171,7 @@ def distances(self, edge_list: Iterable | None = None) -> dict: Distances are calculated directly from the coordinates. Raises an error if there are no coordinates on the graph. - Arguments: + Args: edge_list: set of edges. """ if self.has_coords: @@ -203,7 +209,7 @@ def interaction_matrix(self) -> np.ndarray: def min_distance(self, connected: bool | None = None) -> float: """Returns the minimum distance in the graph. - Arguments: + Args: connected: if True/False, computes only over connected/disconnected nodes. """ distance: float @@ -265,7 +271,7 @@ def is_ud_graph(self) -> bool: def ud_edges(self, radius: float) -> set: """Returns the set of edges given by the intersection of circles of a given radius. - Arguments: + Args: radius: the value """ if self.has_coords: @@ -283,7 +289,7 @@ def rescale_coords( Accepts either a scaling or a spacing factor. - Arguments: + Args: scaling: value to scale by. spacing: value to set as the minimum distance in the graph. """ @@ -303,7 +309,7 @@ def rescale_coords( def set_ud_edges(self, radius: float) -> None: """Reset the set of edges to be equal to the set of unit-disk edges. - Arguments: + Args: radius: the radius to use in determining the set of unit-disk edges. """ self.remove_edges_from(list(self.edges)) diff --git a/qoolqit/graphs/data_graph.py b/qoolqit/graphs/data_graph.py index 6a16e1c2b..23dba9771 100644 --- a/qoolqit/graphs/data_graph.py +++ b/qoolqit/graphs/data_graph.py @@ -23,15 +23,6 @@ class DataGraph(BaseGraph): """The main graph structure to represent problem data.""" - def __init__(self, edges: Iterable = []) -> None: - """ - Default constructor for the BaseGraph. - - Arguments: - edges: set of edge tuples (i, j) - """ - super().__init__(edges) - @classmethod def line(cls, n: int, spacing: float = 1.0) -> DataGraph: """Constructs a line graph, with the respective coordinates. From 3ba054f9dbe333f8189921aa1a95ce20b1bd1608 Mon Sep 17 00:00:00 2001 From: Stefano Grava Date: Mon, 10 Aug 2026 18:06:40 +0200 Subject: [PATCH 06/13] remove comments top file --- qoolqit/graphs/data_graph.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/qoolqit/graphs/data_graph.py b/qoolqit/graphs/data_graph.py index 23dba9771..733a0134a 100644 --- a/qoolqit/graphs/data_graph.py +++ b/qoolqit/graphs/data_graph.py @@ -1,8 +1,3 @@ -# TODO: -# - refactor this to reuse common methods in constructors -# - refactor using rescale_coords method - - from __future__ import annotations from collections.abc import Iterable From d7b56e495f4d72ff5b5d58b4f067657022087052 Mon Sep 17 00:00:00 2001 From: Stefano Grava Date: Wed, 12 Aug 2026 14:59:29 +0200 Subject: [PATCH 07/13] move from_nx --- qoolqit/graphs/base_graph.py | 144 +++++++++++++++++------------------ 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/qoolqit/graphs/base_graph.py b/qoolqit/graphs/base_graph.py index aa39966d5..ba33c1dfc 100644 --- a/qoolqit/graphs/base_graph.py +++ b/qoolqit/graphs/base_graph.py @@ -76,6 +76,78 @@ def from_coordinates(cls, coords: list | dict) -> BaseGraph: return cls.from_nodes(coords_tuple) + @classmethod + def from_nx(cls, g: nx.Graph) -> BaseGraph: + """Convert a NetworkX Graph object into a QoolQit graph instance. + + The input `networkx.Graph` graph must be defined only with the following allowed + + Node attributes: + pos (tuple): represents the node 2D position. Must be a list/tuple of real numbers. + weight: represents the node weight. Must be a real number. + Edge attributes: + weight: represents the edge weight. Must be a real number. + + Returns an instance of the class with following attributes: + - node_weights : dict[node, float or None] + - edge_weights : dict[(u,v), float or None] + - coords : dict[node, (float,float) or None] + """ + if not isinstance(g, nx.Graph): + raise TypeError("Input must be a networkx.Graph instance.") + + g = nx.convert_node_labels_to_integers(g) + num_nodes = len(g.nodes) + num_edges = len(g.edges) + + # validate node attributes + for name, data in g.nodes.data(): + unexpected_keys = set(data) - {"weight", "pos"} + if unexpected_keys: + raise ValueError(f"{unexpected_keys} not allowed in node attributes.") + + node_pos = nx.get_node_attributes(g, "pos") + if node_pos: + if len(node_pos) != num_nodes: + raise ValueError("Node attribute `pos` must be defined for all nodes") + for name, pos in node_pos.items(): + is_2D = isinstance(pos, (tuple, list)) & (len(pos) == 2) + is_real = all(isinstance(p, (float, int)) for p in pos) + if not (is_2D & is_real): + raise TypeError( + f"In node {name} the `pos` attribute must be a 2D tuple/list" + f" of real numbers, got {pos} instead." + ) + node_weights = nx.get_node_attributes(g, "weight") + if node_weights: + if len(node_weights) != num_nodes: + raise ValueError("Node attribute `weight` must be defined for all nodes") + for name, weight in node_weights.items(): + if not isinstance(weight, (float, int)): + raise TypeError( + f"In node {name} the `weight` attribute must be a real number, " + f"got {type(weight)} instead." + "" + ) + + # validate edge attributes + for u, v, data in g.edges.data(): + unexpected_keys = set(data) - {"weight"} + if unexpected_keys: + raise ValueError(f"{unexpected_keys} not allowed in edge attributes.") + edge_weights = nx.get_edge_attributes(g, "weight") + if edge_weights: + if len(edge_weights) != num_edges: + raise ValueError("Edge attribute `weight` must be defined for all edges") + for name, weight in edge_weights.items(): + if not isinstance(weight, (float, int)): + raise TypeError( + f"In edge {name}, the attribute `weight` must be a real number, " + f"got {type(weight)} instead." + ) + + return cls(g) + @classmethod def from_matrix(cls, data: npt.NDArray[np.float64]) -> BaseGraph: """Constructs a graph from a symmetric square matrix. @@ -452,78 +524,6 @@ def set_ud_edges(self, radius: float) -> None: self.remove_edges_from(list(self.edges)) self.add_edges_from(self.ud_edges(radius)) - @classmethod - def from_nx(cls, g: nx.Graph) -> BaseGraph: - """Convert a NetworkX Graph object into a QoolQit graph instance. - - The input `networkx.Graph` graph must be defined only with the following allowed - - Node attributes: - pos (tuple): represents the node 2D position. Must be a list/tuple of real numbers. - weight: represents the node weight. Must be a real number. - Edge attributes: - weight: represents the edge weight. Must be a real number. - - Returns an instance of the class with following attributes: - - node_weights : dict[node, float or None] - - edge_weights : dict[(u,v), float or None] - - coords : dict[node, (float,float) or None] - """ - if not isinstance(g, nx.Graph): - raise TypeError("Input must be a networkx.Graph instance.") - - g = nx.convert_node_labels_to_integers(g) - num_nodes = len(g.nodes) - num_edges = len(g.edges) - - # validate node attributes - for name, data in g.nodes.data(): - unexpected_keys = set(data) - {"weight", "pos"} - if unexpected_keys: - raise ValueError(f"{unexpected_keys} not allowed in node attributes.") - - node_pos = nx.get_node_attributes(g, "pos") - if node_pos: - if len(node_pos) != num_nodes: - raise ValueError("Node attribute `pos` must be defined for all nodes") - for name, pos in node_pos.items(): - is_2D = isinstance(pos, (tuple, list)) & (len(pos) == 2) - is_real = all(isinstance(p, (float, int)) for p in pos) - if not (is_2D & is_real): - raise TypeError( - f"In node {name} the `pos` attribute must be a 2D tuple/list" - f" of real numbers, got {pos} instead." - ) - node_weights = nx.get_node_attributes(g, "weight") - if node_weights: - if len(node_weights) != num_nodes: - raise ValueError("Node attribute `weight` must be defined for all nodes") - for name, weight in node_weights.items(): - if not isinstance(weight, (float, int)): - raise TypeError( - f"In node {name} the `weight` attribute must be a real number, " - f"got {type(weight)} instead." - "" - ) - - # validate edge attributes - for u, v, data in g.edges.data(): - unexpected_keys = set(data) - {"weight"} - if unexpected_keys: - raise ValueError(f"{unexpected_keys} not allowed in edge attributes.") - edge_weights = nx.get_edge_attributes(g, "weight") - if edge_weights: - if len(edge_weights) != num_edges: - raise ValueError("Edge attribute `weight` must be defined for all edges") - for name, weight in edge_weights.items(): - if not isinstance(weight, (float, int)): - raise TypeError( - f"In edge {name}, the attribute `weight` must be a real number, " - f"got {type(weight)} instead." - ) - - return cls(g) - def draw(self, ax: Axes | None = None, **kwargs: Any) -> None: """Draw the graph. From 21a4a6fd051fcefb924026b7a55278cf3b872dad Mon Sep 17 00:00:00 2001 From: Stefano Grava Date: Wed, 12 Aug 2026 15:02:25 +0200 Subject: [PATCH 08/13] remove merge conflicts --- qoolqit/graphs/data_graph.py | 70 ------------------------------------ 1 file changed, 70 deletions(-) diff --git a/qoolqit/graphs/data_graph.py b/qoolqit/graphs/data_graph.py index 33bf427fe..df6d44e2d 100644 --- a/qoolqit/graphs/data_graph.py +++ b/qoolqit/graphs/data_graph.py @@ -250,76 +250,6 @@ def random_ud( graph.add_edges_from(edges) return graph - @classmethod - def from_matrix(cls, data: npt.NDArray[np.float64]) -> DataGraph: - """Constructs a graph from a symmetric square matrix. - - The diagonal values are set as the node weights. For each entry (i, j) - where M[i, j] != 0 an edge (i, j) is added to the graph and the value - M[i, j] is set as its weight. - - Arguments: - data: real symmetric square matrix. - """ - if data.ndim != 2: - raise ValueError("2D Matrix required.") - if not np.allclose(data, data.T, rtol=0.0, atol=1e-7): - raise ValueError("Matrix must be symmetric.") - - # Absolute values below this tolerance are treated as zeros. - # The corresponding node or edge weight is neglected (weight = None). - nonzero_tol = 1e-7 - - diag = np.diag(data) - n_nodes = len(diag) - if np.allclose(diag, np.zeros_like(diag), rtol=0.0, atol=nonzero_tol): - node_weights = None - else: - node_weights = {i: diag[i].item() for i in range(n_nodes)} - - edge_list = [ - (i, j) - for i in range(n_nodes) - for j in range(i + 1, n_nodes) - if (np.abs(data[i, j]) >= nonzero_tol) - ] - edge_weights = {(i, j): data[i, j].item() for i, j in edge_list} - - graph = cls.from_nodes(range(n_nodes)) - graph.add_edges_from(edge_list) - if node_weights is not None: - graph.node_weights = node_weights - graph.edge_weights = edge_weights - return graph - - def to_matrix(self) -> npt.NDArray[np.float64]: - """Return the adjacency matrix of this graph. - - The inverse of `from_matrix`. - Nodes are mapped to indices 0, ..., N-1 according to `self.nodes` insertion order. - - Node weights are stored in the diagonal since self-loops are not supported. - Nodes with no weight set (None) are left at 0.0 in the diagonal. - - For each edge (i, j), the entries (i,j) and (j,i) are set to its weight, - or to 1.0 if the edge has no weight set. - - Returns: - Symmetric N x N matrix of dtype float64, where N is the number of nodes. - """ - n_nodes = len(self.nodes) - index = {node: i for i, node in enumerate(self.nodes)} - matrix = np.zeros((n_nodes, n_nodes), dtype=np.float64) - - for node, weight in self.node_weights.items(): - if weight is not None: - i = index[node] - matrix[i, i] = weight - - for (u, v), weight in self.edge_weights.items(): - i, j = index[u], index[v] - matrix[i, j] = matrix[j, i] = weight if weight is not None else 1.0 - - return matrix - @classmethod def from_pyg( cls, From b503fe4a4ec0ebb5db736ce46295ce872348a5c4 Mon Sep 17 00:00:00 2001 From: Stefano Grava Date: Wed, 12 Aug 2026 15:25:51 +0200 Subject: [PATCH 09/13] rebase --- qoolqit/graphs/base_graph.py | 50 ------------------------------------ 1 file changed, 50 deletions(-) diff --git a/qoolqit/graphs/base_graph.py b/qoolqit/graphs/base_graph.py index ba33c1dfc..c4b1ed2ed 100644 --- a/qoolqit/graphs/base_graph.py +++ b/qoolqit/graphs/base_graph.py @@ -301,56 +301,6 @@ def edge_weights(self, weights: list | dict) -> None: weights_dict = weights nx.set_edge_attributes(self, weights_dict, "weight") - @property - def node_weights(self) -> dict: - """Return the dictionary of node weights.""" - return self._node_weights - - @node_weights.setter - def node_weights(self, weights: list | dict) -> None: - """Set the dictionary of node weights. - - Arguments: - weights: list or dictionary of weights. - """ - if isinstance(weights, list): - if len(weights) != self.number_of_nodes(): - raise ValueError("Size of the weights list does not match the number of nodes.") - weights_dict = {i: w for i, w in zip(self.nodes, weights)} - elif isinstance(weights, dict): - nodes = set(weights.keys()) - if set(self.nodes) != nodes: - raise ValueError( - "Set of nodes in the given dictionary does not match the graph nodes." - ) - weights_dict = weights - self._node_weights = weights_dict - - @property - def edge_weights(self) -> dict: - """Return the dictionary of edge weights.""" - return self._edge_weights - - @edge_weights.setter - def edge_weights(self, weights: list | dict) -> None: - """Set the dictionary of edge weights. - - Arguments: - weights: list or dictionary of weights. - """ - if isinstance(weights, list): - if len(weights) != self.number_of_edges(): - raise ValueError("Size of the weights list does not match the number of nodes.") - weights_dict = {i: w for i, w in zip(self.sorted_edges, weights)} - elif isinstance(weights, dict): - edges = set(weights.keys()) - if set(self.sorted_edges) != edges: - raise ValueError( - "Set of edges in the given dictionary does not match the graph ordered edges." - ) - weights_dict = weights - self._edge_weights = weights_dict - @property def coords(self) -> dict: """Return a dictionary of node coordinates.""" From e2f411363c2366cf43e0ca77e300ea9824d363c0 Mon Sep 17 00:00:00 2001 From: Stefano Grava Date: Wed, 12 Aug 2026 16:29:19 +0200 Subject: [PATCH 10/13] clean up PR --- qoolqit/graphs/base_graph.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/qoolqit/graphs/base_graph.py b/qoolqit/graphs/base_graph.py index c4b1ed2ed..cf7a7f794 100644 --- a/qoolqit/graphs/base_graph.py +++ b/qoolqit/graphs/base_graph.py @@ -48,11 +48,8 @@ class BaseGraph(nx.Graph): def from_nodes(cls, nodes: Iterable) -> BaseGraph: """Construct a base graph from a set of nodes. - Args: - nodes: Iterable container. - Can be a container of nodes (list, dict, set, etc.) or - a container of (node, attribute dict) tuples. - Node attributes are updated using the attribute dict. + Arguments: + nodes: set of nodes. """ graph = cls() graph.add_nodes_from(nodes) @@ -62,11 +59,7 @@ def from_nodes(cls, nodes: Iterable) -> BaseGraph: def from_coordinates(cls, coords: list | dict) -> BaseGraph: """Construct a base graph from a set of coordinates. - From a list of coordinates, nodes are labelled with their index. - From a dictionary, nodes are labelled with their keys. - Each node is added to the graph with its position as a node attribute `pos`. - - Args: + Arguments: coords: list or dictionary of coordinate pairs. """ if isinstance(coords, list): @@ -254,7 +247,7 @@ def has_edge_weights(self) -> bool: @property def node_weights(self) -> dict: """Return the dictionary of node weights.""" - return dict(self.nodes(data="weight")) + return {n: w for n, w in self.nodes(data="weight")} @node_weights.setter def node_weights(self, weights: list | dict) -> None: @@ -310,7 +303,7 @@ def coords(self) -> dict: def coords(self, coords: list | dict) -> None: """Set the dictionary of node coordinates. - Args: + Arguments: coords: list or dictionary of coordinate pairs. """ if isinstance(coords, list): @@ -330,7 +323,7 @@ def distances(self, edge_list: Iterable | None = None) -> dict: Distances are calculated directly from the coordinates. Raises an error if there are no coordinates on the graph. - Args: + Arguments: edge_list: set of edges. """ if self.has_coords: @@ -368,7 +361,7 @@ def interaction_matrix(self) -> np.ndarray: def min_distance(self, connected: bool | None = None) -> float: """Returns the minimum distance in the graph. - Args: + Arguments: connected: if True/False, computes only over connected/disconnected nodes. """ distance: float @@ -430,7 +423,7 @@ def is_ud_graph(self) -> bool: def ud_edges(self, radius: float) -> set: """Returns the set of edges given by the intersection of circles of a given radius. - Args: + Arguments: radius: the value """ if self.has_coords: @@ -448,7 +441,7 @@ def rescale_coords( Accepts either a scaling or a spacing factor. - Args: + Arguments: scaling: value to scale by. spacing: value to set as the minimum distance in the graph. """ @@ -468,7 +461,7 @@ def rescale_coords( def set_ud_edges(self, radius: float) -> None: """Reset the set of edges to be equal to the set of unit-disk edges. - Args: + Arguments: radius: the radius to use in determining the set of unit-disk edges. """ self.remove_edges_from(list(self.edges)) @@ -479,7 +472,7 @@ def draw(self, ax: Axes | None = None, **kwargs: Any) -> None: Uses the draw_networkx function from NetworkX. - Args: + Arguments: ax: Axes object to draw on. If None, uses the current Axes. **kwargs: keyword-arguments to pass to draw_networkx. """ From 95d2551e891bfacb18ba38f34f4a8326a3311a74 Mon Sep 17 00:00:00 2001 From: Stefano Grava Date: Wed, 12 Aug 2026 16:31:03 +0200 Subject: [PATCH 11/13] revert arg change --- qoolqit/graphs/base_graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qoolqit/graphs/base_graph.py b/qoolqit/graphs/base_graph.py index cf7a7f794..8e5f66455 100644 --- a/qoolqit/graphs/base_graph.py +++ b/qoolqit/graphs/base_graph.py @@ -472,7 +472,7 @@ def draw(self, ax: Axes | None = None, **kwargs: Any) -> None: Uses the draw_networkx function from NetworkX. - Arguments: + Args: ax: Axes object to draw on. If None, uses the current Axes. **kwargs: keyword-arguments to pass to draw_networkx. """ From 5899543cccdce5b1688673e9abef641c3d6fa776 Mon Sep 17 00:00:00 2001 From: Stefano Grava Date: Wed, 12 Aug 2026 16:39:18 +0200 Subject: [PATCH 12/13] fix flagged test supposed to fix --- qoolqit/graphs/data_graph.py | 2 +- tests/test_graphs/test_base_graph.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/qoolqit/graphs/data_graph.py b/qoolqit/graphs/data_graph.py index df6d44e2d..d8f08baba 100644 --- a/qoolqit/graphs/data_graph.py +++ b/qoolqit/graphs/data_graph.py @@ -73,7 +73,7 @@ def circle( ] edges = [(i, i + 1) for i in range(n - 1)] + [(n - 1, 0)] graph = cls.from_coordinates(coords) - graph.add_edges_from(edges, weight=1.0) + graph.add_edges_from(edges) return graph @classmethod diff --git a/tests/test_graphs/test_base_graph.py b/tests/test_graphs/test_base_graph.py index aecade7b7..98e08557d 100644 --- a/tests/test_graphs/test_base_graph.py +++ b/tests/test_graphs/test_base_graph.py @@ -209,9 +209,6 @@ def test_from_matrix(n_nodes: int) -> None: def test_to_matrix_unweighted(n_nodes: int) -> None: graph = BaseGraph.from_nodes(range(n_nodes)) graph.add_edges_from(random_edge_list(range(n_nodes), k=2 * n_nodes)) - # FIXME: _edge_weights is a snapshot that goes stale after add_edges_from; - # see issue #431 (edge_weights does not reflect edges added after construction). - graph._reset_dicts() assert not graph.has_node_weights assert not graph.has_edge_weights From dec2a63e8fafc34bf84760037098d827cfdbf7fc Mon Sep 17 00:00:00 2001 From: Stefano Grava Date: Wed, 12 Aug 2026 16:40:49 +0200 Subject: [PATCH 13/13] revert change on line weights --- qoolqit/graphs/data_graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qoolqit/graphs/data_graph.py b/qoolqit/graphs/data_graph.py index d8f08baba..69b238d0d 100644 --- a/qoolqit/graphs/data_graph.py +++ b/qoolqit/graphs/data_graph.py @@ -47,7 +47,7 @@ def line(cls, n: int, spacing: float = 1.0) -> DataGraph: coords = [(i * spacing, 0.0) for i in range(n)] graph = cls.from_coordinates(coords) edges = [(i, i + 1) for i in range(0, n - 1)] - graph.add_edges_from(edges, weight=1.0) + graph.add_edges_from(edges) return graph @classmethod