Skip to content

qoolqit.graphs

graphs

Graph creation and manipulation in QoolQit.

Modules:

  • base_graph

    Base graph class on top of NetworkX: coordinates, weights, matrix conversion, unit-disk utils.

  • data_graph

    Graph structure for problem data, with named topology constructors and PyG interchange.

  • utils

Classes:

  • BaseGraph

    Base graph class, directly inheriting from networkx.Graph.

  • DataGraph

    Graph structure to represent problem data.

Functions:

  • all_node_pairs

    Return all pairs of nodes (u, v) where u < v.

  • distances

    Return a dictionary of edge distances.

  • random_coords

    Generate a random set of node coordinates on a square of side L.

  • random_edge_list

    Generates a random set of k edges linkings items from a set of nodes.

  • scale_coords

    Scale the coordinates by a given value.

  • space_coords

    Spaces the coordinates so the minimum distance is equal to a set spacing.

BaseGraph

Base graph class, directly inheriting from networkx.Graph.

Represents a simple graph (undirected and without self-loops), with optional node coordinates and node/edge weights.

On top of the standard networkx.Graph functionality, adds alternative constructors, node coordinates and weights as first-class attributes, distance and Rydberg-interaction calculations, unit-disk graph analysis, and plotting.

Attributes:

  • coords (dict) –

    Dict mapping each node to its 2D coordinate, or None if unset.

  • node_weights (dict) –

    Dict mapping each node to its weight, or None if unset.

  • edge_weights (dict) –

    Dict mapping each edge to its weight, or None if unset.

Note

Alternative constructors: from_nodes, from_coordinates, from_nx, from_matrix (with its inverse to_matrix). Coordinates and distances: coords, distances, min_distance, max_distance, rescale_coords. Unit-disk analysis: is_ud_graph, ud_radius_range, ud_edges, set_ud_edges. Rydberg analog model utils: interactions, interaction_matrix. Plotting: draw.

Methods:

  • distances

    Returns a dictionary of distances for a given set of edges.

  • draw

    Draw the graph.

  • from_coordinates

    Construct a base graph from a set of coordinates.

  • from_matrix

    Constructs a graph from a symmetric square matrix.

  • from_nodes

    Construct a base graph from a set of nodes.

  • from_nx

    Convert a NetworkX Graph object into a QoolQit graph instance.

  • interaction_matrix

    Rydberg model interaction 1/r^6 between pairs of nodes, as a matrix.

  • interactions

    Rydberg model interaction 1/r^6 between pair of nodes.

  • is_ud_graph

    Check if the graph is unit-disk.

  • max_distance

    Returns the maximum distance in the graph.

  • min_distance

    Returns the minimum distance in the graph.

  • rescale_coords

    Rescales the node coordinates by a factor.

  • set_ud_edges

    Reset the set of edges to be equal to the set of unit-disk edges.

  • to_matrix

    Return the adjacency matrix of this graph.

  • ud_edges

    Returns the set of edges whose distance is at most the given radius.

  • ud_radius_range

    Return the range (R_min, R_max) where the graph is unit-disk.

all_node_pairs property

all_node_pairs: set

Return a list of all possible node pairs in the graph.

coords property writable

coords: dict

Return a dictionary of node coordinates.

edge_weights property writable

edge_weights: dict

Return the dictionary of edge weights.

has_coords property

has_coords: bool

Check if the graph has coordinates on all nodes.

has_edge_weights property

has_edge_weights: bool

Check if the graph has edge weights on all edges.

has_node_weights property

has_node_weights: bool

Check if the graph has node weights on all nodes.

node_weights property writable

node_weights: dict

Return the dictionary of node weights.

sorted_edges property

sorted_edges: set

Returns the set of edges (u, v) such that (u < v).

distances

distances(edge_list: Iterable | None = None) -> dict

Returns a dictionary of distances for a given set of edges.

Distances are calculated directly from the coordinates. Raises an error if there are no coordinates on the graph.

Parameters:

  • edge_list (Iterable | None, default: None ) –

    set of edges.

Source code in qoolqit/graphs/base_graph.py
def distances(self, edge_list: Iterable | None = None) -> dict:
    """Returns a dictionary of distances for a given set of edges.

    Distances are calculated directly from the coordinates. Raises an error
    if there are no coordinates on the graph.

    Arguments:
        edge_list: set of edges.
    """
    if self.has_coords:
        if edge_list is None:
            edge_list = self.all_node_pairs
        elif len(edge_list) == 0:  # type: ignore [arg-type]
            raise ValueError("Trying to compute distances for an empty edge list.")
        return distances(self.coords, edge_list)
    else:
        raise AttributeError("Trying to compute distances for a graph without coordinates.")

draw

draw(ax: Axes | None = None, **kwargs: Any) -> None

Draw the graph.

Uses the draw_networkx function from NetworkX.

Parameters:

  • ax (Axes | None, default: None ) –

    Axes object to draw on. If None, uses the current Axes.

  • **kwargs (Any, default: {} ) –

    keyword-arguments to pass to draw_networkx.

Source code in qoolqit/graphs/base_graph.py
def draw(self, ax: Axes | None = None, **kwargs: Any) -> None:
    """Draw the graph.

    Uses the draw_networkx function from NetworkX.

    Args:
        ax: Axes object to draw on. If None, uses the current Axes.
        **kwargs: keyword-arguments to pass to draw_networkx.
    """
    if self.has_coords:
        if "hide_ticks" not in kwargs:
            kwargs["hide_ticks"] = False

        nx.draw_networkx(self, pos=self.coords, ax=ax, **kwargs)

        if ax is None:
            ax = plt.gca()
        ax.set_xlabel("x")
        ax.set_ylabel("y")
        ax.grid(True, color="lightgray", linestyle="--", linewidth=0.7)

        # minimum ybox
        ylim = ax.get_ylim()
        if (ylim[1] - ylim[0]) < 2:
            y_center = (ylim[0] + ylim[1]) / 2
            ax.set_ylim(y_center - 1, y_center + 1)
        plt.tight_layout()
    else:
        nx.draw_networkx(self, ax=ax, **kwargs)

from_coordinates classmethod

from_coordinates(coords: list | dict) -> BaseGraph

Construct a base graph from a set of coordinates.

Parameters:

  • coords (list | dict) –

    list or dictionary of coordinate pairs.

Source code in qoolqit/graphs/base_graph.py
@classmethod
def from_coordinates(cls, coords: list | dict) -> BaseGraph:
    """Construct a base graph from a set of coordinates.

    Arguments:
        coords: list or dictionary of coordinate pairs.
    """
    if isinstance(coords, list):
        coords_tuple = ((i, {"pos": pos}) for i, pos in enumerate(coords))
    elif isinstance(coords, dict):
        coords_tuple = ((key, {"pos": pos}) for key, pos in coords.items())

    return cls.from_nodes(coords_tuple)

from_matrix classmethod

from_matrix(data: NDArray[float64]) -> BaseGraph

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.

Parameters:

  • data (NDArray[float64]) –

    real symmetric square matrix.

Source code in qoolqit/graphs/base_graph.py
@classmethod
def from_matrix(cls, data: npt.NDArray[np.float64]) -> BaseGraph:
    """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(n_nodes), rtol=0.0, atol=nonzero_tol):
        node_weights = {i: None for i in range(n_nodes)}
    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)
    graph.node_weights = node_weights
    graph.edge_weights = edge_weights
    return graph

from_nodes classmethod

from_nodes(nodes: Iterable) -> BaseGraph

Construct a base graph from a set of nodes.

Parameters:

  • nodes (Iterable) –

    set of nodes.

Source code in qoolqit/graphs/base_graph.py
@classmethod
def from_nodes(cls, nodes: Iterable) -> BaseGraph:
    """Construct a base graph from a set of nodes.

    Arguments:
        nodes: set of nodes.
    """
    graph = cls()
    graph.add_nodes_from(nodes)
    return graph

from_nx classmethod

from_nx(g: 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]
Source code in qoolqit/graphs/base_graph.py
@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)

interaction_matrix

interaction_matrix() -> ndarray

Rydberg model interaction 1/r^6 between pairs of nodes, as a matrix.

Node ordering follows self.nodes insertion order. The diagonal is 0, since there is no self-interaction.

Returns:

  • ndarray

    Symmetric N x N matrix of dtype float64, where N is the number of nodes.

Source code in qoolqit/graphs/base_graph.py
def interaction_matrix(self) -> np.ndarray:
    """Rydberg model interaction 1/r^6 between pairs of nodes, as a matrix.

    Node ordering follows `self.nodes` insertion order.
    The diagonal is 0, since there is no self-interaction.

    Returns:
        Symmetric N x N matrix of dtype float64, where N is the number of nodes.
    """
    index = {node: i for i, node in enumerate(self.nodes)}
    n_nodes = len(index)
    matrix = np.zeros((n_nodes, n_nodes), dtype=np.float64)

    for (u, v), interaction in self.interactions().items():
        i, j = index[u], index[v]
        matrix[i, j] = matrix[j, i] = interaction

    return matrix

interactions

interactions() -> dict

Rydberg model interaction 1/r^6 between pair of nodes.

Source code in qoolqit/graphs/base_graph.py
def interactions(self) -> dict:
    """Rydberg model interaction 1/r^6 between pair of nodes."""
    return {p: 1.0 / (r**6) for p, r in self.distances().items()}

is_ud_graph

is_ud_graph() -> bool

Check if the graph is unit-disk.

Source code in qoolqit/graphs/base_graph.py
def is_ud_graph(self) -> bool:
    """Check if the graph is unit-disk."""
    try:
        self.ud_radius_range()
        return True
    except ValueError:
        return False

max_distance

max_distance(connected: bool | None = None) -> float

Returns the maximum distance in the graph.

Parameters:

  • connected (bool | None, default: None ) –

    if True/False, computes only over connected/disconnected nodes.

Source code in qoolqit/graphs/base_graph.py
def max_distance(self, connected: bool | None = None) -> float:
    """Returns the maximum distance in the graph.

    Arguments:
        connected: if True/False, computes only over connected/disconnected nodes.
    """
    distance: float
    if connected is None:
        distance = max(self.distances(self.all_node_pairs).values())
    elif connected:
        distance = max(self.distances(self.sorted_edges).values())
    else:
        distance = max(self.distances(self.all_node_pairs - self.sorted_edges).values())
    return distance

min_distance

min_distance(connected: bool | None = None) -> float

Returns the minimum distance in the graph.

Parameters:

  • connected (bool | None, default: None ) –

    if True/False, computes only over connected/disconnected nodes.

Source code in qoolqit/graphs/base_graph.py
def min_distance(self, connected: bool | None = None) -> float:
    """Returns the minimum distance in the graph.

    Arguments:
        connected: if True/False, computes only over connected/disconnected nodes.
    """
    distance: float
    if connected is None:
        distance = min(self.distances(self.all_node_pairs).values())
    elif connected:
        distance = min(self.distances(self.sorted_edges).values())
    else:
        distance = min(self.distances(self.all_node_pairs - self.sorted_edges).values())
    return distance

rescale_coords

rescale_coords(
    *args: Any,
    scaling: float | None = None,
    spacing: float | None = None,
) -> None

Rescales the node coordinates by a factor.

Accepts either a scaling or a spacing factor.

Parameters:

  • scaling (float | None, default: None ) –

    value to scale by.

  • spacing (float | None, default: None ) –

    value to set as the minimum distance in the graph.

Source code in qoolqit/graphs/base_graph.py
def rescale_coords(
    self,
    *args: Any,
    scaling: float | None = None,
    spacing: float | None = None,
) -> None:
    """Rescales the node coordinates by a factor.

    Accepts either a scaling or a spacing factor.

    Arguments:
        scaling: value to scale by.
        spacing: value to set as the minimum distance in the graph.
    """
    if self.has_coords:
        msg = "Please pass either a `scaling` or a `spacing` value as a keyword argument."
        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)
        elif spacing is None and scaling is not None:
            self.coords = scale_coords(self.coords, scaling)
        else:
            raise TypeError(msg)
    else:
        raise AttributeError("Trying to rescale coordinates on a graph without coordinates.")

set_ud_edges

set_ud_edges(radius: float) -> None

Reset the set of edges to be equal to the set of unit-disk edges.

Parameters:

  • radius (float) –

    the radius to use in determining the set of unit-disk edges.

Source code in qoolqit/graphs/base_graph.py
def set_ud_edges(self, radius: float) -> None:
    """Reset the set of edges to be equal to the set of unit-disk edges.

    Arguments:
        radius: the radius to use in determining the set of unit-disk edges.
    """
    self.remove_edges_from(list(self.edges))
    self.add_edges_from(self.ud_edges(radius))

to_matrix

to_matrix() -> NDArray[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:

  • NDArray[float64]

    Symmetric N x N matrix of dtype float64, where N is the number of nodes.

Source code in qoolqit/graphs/base_graph.py
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

ud_edges

ud_edges(radius: float, tol: float = 1e-07) -> set

Returns the set of edges whose distance is at most the given radius.

Parameters:

  • radius (float) –

    the unit-disk radius; a pair of nodes is included if their distance is at most this value.

  • tol (float, default: 1e-07 ) –

    numerical tolerance added to the radius to account for floating point error.

Source code in qoolqit/graphs/base_graph.py
def ud_edges(self, radius: float, tol: float = 1e-7) -> set:
    """Returns the set of edges whose distance is at most the given radius.

    Arguments:
        radius: the unit-disk radius; a pair of nodes is included if their distance is at
            most this value.
        tol: numerical tolerance added to the radius to account for floating point error.
    """
    if radius < 0:
        raise ValueError("`radius` must be a non-negative value.")
    if not self.has_coords:
        raise AttributeError(
            "Trying to compute unit-disk edges for a graph without coordinates."
        )

    return set(e for e, d in self.distances().items() if d <= radius + tol)

ud_radius_range

ud_radius_range() -> tuple

Return the range (R_min, R_max) where the graph is unit-disk.

The graph is unit-disk if the maximum distance between all connected nodes is smaller than the minimum distance between disconnected nodes. This means that for any value R in that interval, the following condition is true:

graph.ud_edges(radius = R) == graph.sorted edges

Source code in qoolqit/graphs/base_graph.py
def ud_radius_range(self) -> tuple:
    """Return the range (R_min, R_max) where the graph is unit-disk.

    The graph is unit-disk if the maximum distance between all connected nodes is
    smaller than the minimum distance between disconnected nodes. This means that
    for any value R in that interval, the following condition is true:

    graph.ud_edges(radius = R) == graph.sorted edges
    """
    if self.has_coords:
        n_edges = len(self.sorted_edges)
        if n_edges == 0:
            # If the graph is empty and has coordinates
            return (0.0, self.min_distance(connected=False))
        elif n_edges == len(self.all_node_pairs):
            # If the graph is fully connected
            return (self.max_distance(connected=True), float("inf"))
        elif self.max_distance(connected=True) < self.min_distance(connected=False):
            return (self.max_distance(connected=True), self.min_distance(connected=False))
        else:
            raise ValueError("Graph is not unit disk.")
    else:
        raise AttributeError("Checking if graph is unit disk is not valid without coordinates.")

DataGraph

Graph structure to represent problem data.

Represents a simple graph (undirected and without self-loops), with node coordinates and node/edge weights, distance and Rydberg-interaction calculations, conversion to/from adjacency matrix, unit-disk graph analysis, and plotting (inherited from BaseGraph). Adds named topology constructors and conversion to/from PyTorch Geometric data objects.

Attributes:

  • coords (dict) –

    Dict mapping each node to its 2D coordinate, or None if unset.

  • node_weights (dict) –

    Dict mapping each node to its weight, or None if unset.

  • edge_weights (dict) –

    Dict mapping each edge to its weight, or None if unset.

Note

Named topology constructors: line, circle, triangular, hexagonal, heavy_hexagonal, square, random_er, random_ud. PyTorch Geometric conversion: from_pyg, to_pyg.

Methods:

  • circle

    Constructs a circle graph, with the respective coordinates.

  • distances

    Returns a dictionary of distances for a given set of edges.

  • draw

    Draw the graph.

  • from_coordinates

    Construct a base graph from a set of coordinates.

  • from_matrix

    Constructs a graph from a symmetric square matrix.

  • from_nodes

    Construct a base graph from a set of nodes.

  • from_nx

    Convert a NetworkX Graph object into a QoolQit graph instance.

  • from_pyg

    Convert a PyTorch Geometric Data object into a DataGraph instance.

  • heavy_hexagonal

    Constructs a heavy-hexagonal lattice graph, with respective coordinates.

  • hexagonal

    Constructs a hexagonal lattice graph, with respective coordinates.

  • interaction_matrix

    Rydberg model interaction 1/r^6 between pairs of nodes, as a matrix.

  • interactions

    Rydberg model interaction 1/r^6 between pair of nodes.

  • is_ud_graph

    Check if the graph is unit-disk.

  • line

    Constructs a line graph, with the respective coordinates.

  • max_distance

    Returns the maximum distance in the graph.

  • min_distance

    Returns the minimum distance in the graph.

  • random_er

    Constructs an Erdős-Rényi random graph.

  • random_ud

    Constructs a random unit-disk graph.

  • rescale_coords

    Rescales the node coordinates by a factor.

  • set_ud_edges

    Reset the set of edges to be equal to the set of unit-disk edges.

  • square

    Constructs a square lattice graph, with respective coordinates.

  • to_matrix

    Return the adjacency matrix of this graph.

  • to_pyg

    Convert the DataGraph to a PyTorch Geometric Data object.

  • triangular

    Constructs a triangular lattice graph, with respective coordinates.

  • ud_edges

    Returns the set of edges whose distance is at most the given radius.

  • ud_radius_range

    Return the range (R_min, R_max) where the graph is unit-disk.

all_node_pairs property

all_node_pairs: set

Return a list of all possible node pairs in the graph.

coords property writable

coords: dict

Return a dictionary of node coordinates.

edge_weights property writable

edge_weights: dict

Return the dictionary of edge weights.

has_coords property

has_coords: bool

Check if the graph has coordinates on all nodes.

has_edge_weights property

has_edge_weights: bool

Check if the graph has edge weights on all edges.

has_node_weights property

has_node_weights: bool

Check if the graph has node weights on all nodes.

node_weights property writable

node_weights: dict

Return the dictionary of node weights.

sorted_edges property

sorted_edges: set

Returns the set of edges (u, v) such that (u < v).

circle classmethod

circle(
    n: int, spacing: float = 1.0, center: tuple = (0.0, 0.0)
) -> DataGraph

Constructs a circle graph, with the respective coordinates.

Parameters:

  • n (int) –

    number of nodes.

  • spacing (float, default: 1.0 ) –

    distance between each node.

  • center (tuple, default: (0.0, 0.0) ) –

    point (x, y) to set as the center of the graph.

Source code in qoolqit/graphs/data_graph.py
@classmethod
def circle(
    cls,
    n: int,
    spacing: float = 1.0,
    center: tuple = (0.0, 0.0),
) -> DataGraph:
    """Constructs a circle graph, with the respective coordinates.

    Arguments:
        n: number of nodes.
        spacing: distance between each node.
        center: point (x, y) to set as the center of the graph.
    """

    d_theta = (2.0 * np.pi) / n
    r = spacing / (2.0 * np.sin(np.pi / n))
    theta = np.linspace(0.0, 2.0 * np.pi - d_theta, n)
    coords = [
        (x + center[0], y + center[1]) for x, y in zip(r * np.cos(theta), r * np.sin(theta))
    ]
    edges = [(i, i + 1) for i in range(n - 1)] + [(n - 1, 0)]
    graph = cls.from_coordinates(coords)
    graph.add_edges_from(edges)
    return graph

distances

distances(edge_list: Iterable | None = None) -> dict

Returns a dictionary of distances for a given set of edges.

Distances are calculated directly from the coordinates. Raises an error if there are no coordinates on the graph.

Parameters:

  • edge_list (Iterable | None, default: None ) –

    set of edges.

Source code in qoolqit/graphs/base_graph.py
def distances(self, edge_list: Iterable | None = None) -> dict:
    """Returns a dictionary of distances for a given set of edges.

    Distances are calculated directly from the coordinates. Raises an error
    if there are no coordinates on the graph.

    Arguments:
        edge_list: set of edges.
    """
    if self.has_coords:
        if edge_list is None:
            edge_list = self.all_node_pairs
        elif len(edge_list) == 0:  # type: ignore [arg-type]
            raise ValueError("Trying to compute distances for an empty edge list.")
        return distances(self.coords, edge_list)
    else:
        raise AttributeError("Trying to compute distances for a graph without coordinates.")

draw

draw(ax: Axes | None = None, **kwargs: Any) -> None

Draw the graph.

Uses the draw_networkx function from NetworkX.

Parameters:

  • ax (Axes | None, default: None ) –

    Axes object to draw on. If None, uses the current Axes.

  • **kwargs (Any, default: {} ) –

    keyword-arguments to pass to draw_networkx.

Source code in qoolqit/graphs/base_graph.py
def draw(self, ax: Axes | None = None, **kwargs: Any) -> None:
    """Draw the graph.

    Uses the draw_networkx function from NetworkX.

    Args:
        ax: Axes object to draw on. If None, uses the current Axes.
        **kwargs: keyword-arguments to pass to draw_networkx.
    """
    if self.has_coords:
        if "hide_ticks" not in kwargs:
            kwargs["hide_ticks"] = False

        nx.draw_networkx(self, pos=self.coords, ax=ax, **kwargs)

        if ax is None:
            ax = plt.gca()
        ax.set_xlabel("x")
        ax.set_ylabel("y")
        ax.grid(True, color="lightgray", linestyle="--", linewidth=0.7)

        # minimum ybox
        ylim = ax.get_ylim()
        if (ylim[1] - ylim[0]) < 2:
            y_center = (ylim[0] + ylim[1]) / 2
            ax.set_ylim(y_center - 1, y_center + 1)
        plt.tight_layout()
    else:
        nx.draw_networkx(self, ax=ax, **kwargs)

from_coordinates classmethod

from_coordinates(coords: list | dict) -> BaseGraph

Construct a base graph from a set of coordinates.

Parameters:

  • coords (list | dict) –

    list or dictionary of coordinate pairs.

Source code in qoolqit/graphs/base_graph.py
@classmethod
def from_coordinates(cls, coords: list | dict) -> BaseGraph:
    """Construct a base graph from a set of coordinates.

    Arguments:
        coords: list or dictionary of coordinate pairs.
    """
    if isinstance(coords, list):
        coords_tuple = ((i, {"pos": pos}) for i, pos in enumerate(coords))
    elif isinstance(coords, dict):
        coords_tuple = ((key, {"pos": pos}) for key, pos in coords.items())

    return cls.from_nodes(coords_tuple)

from_matrix classmethod

from_matrix(data: NDArray[float64]) -> BaseGraph

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.

Parameters:

  • data (NDArray[float64]) –

    real symmetric square matrix.

Source code in qoolqit/graphs/base_graph.py
@classmethod
def from_matrix(cls, data: npt.NDArray[np.float64]) -> BaseGraph:
    """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(n_nodes), rtol=0.0, atol=nonzero_tol):
        node_weights = {i: None for i in range(n_nodes)}
    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)
    graph.node_weights = node_weights
    graph.edge_weights = edge_weights
    return graph

from_nodes classmethod

from_nodes(nodes: Iterable) -> BaseGraph

Construct a base graph from a set of nodes.

Parameters:

  • nodes (Iterable) –

    set of nodes.

Source code in qoolqit/graphs/base_graph.py
@classmethod
def from_nodes(cls, nodes: Iterable) -> BaseGraph:
    """Construct a base graph from a set of nodes.

    Arguments:
        nodes: set of nodes.
    """
    graph = cls()
    graph.add_nodes_from(nodes)
    return graph

from_nx classmethod

from_nx(g: 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]
Source code in qoolqit/graphs/base_graph.py
@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)

from_pyg classmethod

from_pyg(
    data: Data,
    node_attrs: Iterable[str] | None = None,
    edge_attrs: Iterable[str] | None = None,
    graph_attrs: Iterable[str] | None = None,
    node_weights_attr: str | None = None,
    edge_weights_attr: str | None = None,
) -> DataGraph

Convert a PyTorch Geometric Data object into a DataGraph instance.

Requires torch_geometric. Uses to_networkx internally.

Default attributes copied (if present on data ):

  • 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 populated automatically — use the explicit parameters:

  • node_weights_attr: real-valued tensor of shape (N,) or (N, 1). Defaults to None.
  • edge_weights_attr: real-valued tensor of shape (E,) or (E, 1) where E = edge_index.shape[1] (directed count). Defaults to None.

The weight attribute is also stored as a regular node/edge attribute.

Parameters:

  • data (Data) –

    PyTorch Geometric Data object to convert.

  • node_attrs (Iterable[str] | None, default: None ) –

    extra node attributes to copy (beyond x and pos).

  • edge_attrs (Iterable[str] | None, default: None ) –

    extra edge attributes to copy (beyond edge_attr).

  • graph_attrs (Iterable[str] | None, default: None ) –

    extra graph-level attributes to copy (beyond y).

  • node_weights_attr (str | None, default: None ) –

    Data attribute to use as node weights.

  • edge_weights_attr (str | None, default: None ) –

    Data attribute to use as edge weights.

Returns:

  • DataGraph

    DataGraph with coords, node_weights, edge_weights

  • DataGraph

    populated where applicable.

Raises:

  • ImportError

    if torch_geometric is not installed.

  • TypeError

    if data is not a torch_geometric.data.Data instance, or if a weight attribute is not a torch.Tensor.

  • AttributeError

    if a specified weight attribute is missing.

  • ValueError

    if a weight tensor has an incompatible shape or size.

Source code in qoolqit/graphs/data_graph.py
@classmethod
def from_pyg(
    cls,
    data: torch_geometric.data.Data,
    node_attrs: Iterable[str] | None = None,
    edge_attrs: Iterable[str] | None = None,
    graph_attrs: Iterable[str] | None = None,
    node_weights_attr: str | None = None,
    edge_weights_attr: str | None = None,
) -> DataGraph:
    """Convert a PyTorch Geometric Data object into a DataGraph instance.

    Requires ``torch_geometric``. Uses ``to_networkx`` internally.

    **Default attributes copied (if present on** ``data`` **):**

    - 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
    populated automatically — use the explicit parameters:

    - ``node_weights_attr``: real-valued tensor of shape ``(N,)`` or
      ``(N, 1)``. Defaults to ``None``.
    - ``edge_weights_attr``: real-valued tensor of shape ``(E,)`` or
      ``(E, 1)`` where ``E = edge_index.shape[1]`` (directed count).
      Defaults to ``None``.

    The weight attribute is also stored as a regular node/edge attribute.

    Arguments:
        data: PyTorch Geometric Data object to convert.
        node_attrs: extra node attributes to copy (beyond x and pos).
        edge_attrs: extra edge attributes to copy (beyond edge_attr).
        graph_attrs: extra graph-level attributes to copy (beyond y).
        node_weights_attr: Data attribute to use as node weights.
        edge_weights_attr: Data attribute to use as edge weights.

    Returns:
        DataGraph with ``coords``, ``node_weights``, ``edge_weights``
        populated where applicable.

    Raises:
        ImportError: if ``torch_geometric`` is not installed.
        TypeError: if ``data`` is not a ``torch_geometric.data.Data``
            instance, or if a weight attribute is not a ``torch.Tensor``.
        AttributeError: if a specified weight attribute is missing.
        ValueError: if a weight tensor has an incompatible shape or size.
    """
    try:
        from torch_geometric.data import Data
        from torch_geometric.utils import to_networkx
    except ImportError as e:
        raise ImportError("Please, install the `torch_geometric` package.") from e

    if not isinstance(data, Data):
        raise TypeError("Input must be a torch_geometric.data.Data object.")

    # Validate weight attrs early and keep the squeezed tensors
    node_tensor = (
        cls._validate_weights_attr(data, node_weights_attr, data.num_nodes, "node")
        if node_weights_attr is not None
        else None
    )
    edge_tensor = (
        cls._validate_weights_attr(data, edge_weights_attr, data.num_edges, "edge")
        if edge_weights_attr is not None
        else None
    )

    # Select unique attributes and add default ones only if present in the data
    node_attrs_set = {k for k in {"x", "pos"} if k in data}
    if node_attrs is not None:
        node_attrs_set |= set(node_attrs)
    if node_weights_attr is not None:
        node_attrs_set.add(node_weights_attr)

    edge_attrs_set = {k for k in {"edge_attr"} if k in data}
    if edge_attrs is not None:
        edge_attrs_set |= set(edge_attrs)
    if edge_weights_attr is not None:
        edge_attrs_set.add(edge_weights_attr)

    graph_attrs_set = {k for k in {"y"} if k in data}
    if graph_attrs is not None:
        graph_attrs_set |= set(graph_attrs)

    # Convert to NetworkX (undirected, no self-loops)
    nx_graph = to_networkx(
        data,
        node_attrs=list(node_attrs_set),
        edge_attrs=list(edge_attrs_set),
        graph_attrs=list(graph_attrs_set),
        to_undirected=True,
        remove_self_loops=True,
    )

    # Build the DataGraph: edges carry their data, nodes carry their data
    graph = cls(nx_graph)

    # node_weights_attr → node_weights
    if node_tensor is not None:
        for i in range(data.num_nodes):
            graph.nodes[i]["weight"] = node_tensor[i].item()

    # edge_weights_attr → edge_weights
    if edge_tensor is not None:
        seen: set = set()
        for idx in range(data.edge_index.shape[1]):
            u = int(data.edge_index[0, idx].item())
            v = int(data.edge_index[1, idx].item())
            key = (min(u, v), max(u, v))
            if key not in seen:
                graph.edges[key]["weight"] = edge_tensor[idx].item()
                seen.add(key)

    return graph

heavy_hexagonal classmethod

heavy_hexagonal(
    m: int, n: int, spacing: float = 1.0
) -> DataGraph

Constructs a heavy-hexagonal lattice graph, with respective coordinates.

Parameters:

  • m (int) –

    Number of rows of hexagons.

  • n (int) –

    Number of columns of hexagons.

  • spacing (float, default: 1.0 ) –

    The distance between adjacent nodes on the final lattice.

Notes

The heavy-hexagonal lattice is a regular hexagonal lattice where each edge is decorated with an additional lattice site.

Source code in qoolqit/graphs/data_graph.py
@classmethod
def heavy_hexagonal(
    cls,
    m: int,
    n: int,
    spacing: float = 1.0,
) -> DataGraph:
    """
    Constructs a heavy-hexagonal lattice graph, with respective coordinates.

    Arguments:
        m: Number of rows of hexagons.
        n: Number of columns of hexagons.
        spacing: The distance between adjacent nodes on the final lattice.

    Notes:
        The heavy-hexagonal lattice is a regular hexagonal lattice where
        each edge is decorated with an additional lattice site.
    """
    G_hex = nx.hexagonal_lattice_graph(m, n, with_positions=True)
    pos_unit = nx.get_node_attributes(G_hex, "pos")

    G_heavy = nx.Graph()
    scaling_factor = 2 * spacing

    label_map = {}
    for old_label, (x, y) in pos_unit.items():
        # Relabel to an even-integer grid to make space for midpoint nodes
        new_label = (2 * old_label[0], 2 * old_label[1])
        label_map[old_label] = new_label

        # Scale positions and add the node to the new graph
        new_pos = (x * scaling_factor, y * scaling_factor)
        G_heavy.add_node(new_label, pos=new_pos)

    for u_old, v_old in G_hex.edges():
        u_new, v_new = label_map[u_old], label_map[v_old]

        mid_label = ((u_new[0] + v_new[0]) // 2, (u_new[1] + v_new[1]) // 2)

        pos_u = G_heavy.nodes[u_new]["pos"]
        pos_v = G_heavy.nodes[v_new]["pos"]
        mid_pos = ((pos_u[0] + pos_v[0]) / 2, (pos_u[1] + pos_v[1]) / 2)

        G_heavy.add_node(mid_label, pos=mid_pos)
        G_heavy.add_edge(u_new, mid_label)
        G_heavy.add_edge(mid_label, v_new)

    final_nodes = sorted(list(G_heavy.nodes()))

    final_coords = [G_heavy.nodes[label]["pos"] for label in final_nodes]

    label_to_int = {label: i for i, label in enumerate(final_nodes)}

    final_edges = [(label_to_int[u], label_to_int[v]) for u, v in G_heavy.edges()]

    graph = cls.from_coordinates(final_coords)
    graph.add_edges_from(final_edges)
    return graph

hexagonal classmethod

hexagonal(
    m: int, n: int, spacing: float = 1.0
) -> DataGraph

Constructs a hexagonal lattice graph, with respective coordinates.

Parameters:

  • m (int) –

    Number of rows of hexagons.

  • n (int) –

    Number of columns of hexagons.

  • spacing (float, default: 1.0 ) –

    The distance between adjacent nodes on the final lattice.

Source code in qoolqit/graphs/data_graph.py
@classmethod
def hexagonal(
    cls,
    m: int,
    n: int,
    spacing: float = 1.0,
) -> DataGraph:
    """
    Constructs a hexagonal lattice graph, with respective coordinates.

    Arguments:
        m: Number of rows of hexagons.
        n: Number of columns of hexagons.
        spacing: The distance between adjacent nodes on the final lattice.
    """
    G = nx.hexagonal_lattice_graph(m, n, with_positions=True)
    G = nx.convert_node_labels_to_integers(G)
    pos_unit = nx.get_node_attributes(G, "pos")

    final_pos = {node: (x * spacing, y * spacing) for node, (x, y) in pos_unit.items()}

    graph = cls.from_coordinates(final_pos)
    graph.add_edges_from(G.edges)
    return graph

interaction_matrix

interaction_matrix() -> ndarray

Rydberg model interaction 1/r^6 between pairs of nodes, as a matrix.

Node ordering follows self.nodes insertion order. The diagonal is 0, since there is no self-interaction.

Returns:

  • ndarray

    Symmetric N x N matrix of dtype float64, where N is the number of nodes.

Source code in qoolqit/graphs/base_graph.py
def interaction_matrix(self) -> np.ndarray:
    """Rydberg model interaction 1/r^6 between pairs of nodes, as a matrix.

    Node ordering follows `self.nodes` insertion order.
    The diagonal is 0, since there is no self-interaction.

    Returns:
        Symmetric N x N matrix of dtype float64, where N is the number of nodes.
    """
    index = {node: i for i, node in enumerate(self.nodes)}
    n_nodes = len(index)
    matrix = np.zeros((n_nodes, n_nodes), dtype=np.float64)

    for (u, v), interaction in self.interactions().items():
        i, j = index[u], index[v]
        matrix[i, j] = matrix[j, i] = interaction

    return matrix

interactions

interactions() -> dict

Rydberg model interaction 1/r^6 between pair of nodes.

Source code in qoolqit/graphs/base_graph.py
def interactions(self) -> dict:
    """Rydberg model interaction 1/r^6 between pair of nodes."""
    return {p: 1.0 / (r**6) for p, r in self.distances().items()}

is_ud_graph

is_ud_graph() -> bool

Check if the graph is unit-disk.

Source code in qoolqit/graphs/base_graph.py
def is_ud_graph(self) -> bool:
    """Check if the graph is unit-disk."""
    try:
        self.ud_radius_range()
        return True
    except ValueError:
        return False

line classmethod

line(n: int, spacing: float = 1.0) -> DataGraph

Constructs a line graph, with the respective coordinates.

Parameters:

  • n (int) –

    number of nodes.

  • spacing (float, default: 1.0 ) –

    distance between each node.

Source code in qoolqit/graphs/data_graph.py
@classmethod
def line(cls, n: int, spacing: float = 1.0) -> DataGraph:
    """Constructs a line graph, with the respective coordinates.

    Arguments:
        n: number of nodes.
        spacing: distance between each node.
    """
    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)
    return graph

max_distance

max_distance(connected: bool | None = None) -> float

Returns the maximum distance in the graph.

Parameters:

  • connected (bool | None, default: None ) –

    if True/False, computes only over connected/disconnected nodes.

Source code in qoolqit/graphs/base_graph.py
def max_distance(self, connected: bool | None = None) -> float:
    """Returns the maximum distance in the graph.

    Arguments:
        connected: if True/False, computes only over connected/disconnected nodes.
    """
    distance: float
    if connected is None:
        distance = max(self.distances(self.all_node_pairs).values())
    elif connected:
        distance = max(self.distances(self.sorted_edges).values())
    else:
        distance = max(self.distances(self.all_node_pairs - self.sorted_edges).values())
    return distance

min_distance

min_distance(connected: bool | None = None) -> float

Returns the minimum distance in the graph.

Parameters:

  • connected (bool | None, default: None ) –

    if True/False, computes only over connected/disconnected nodes.

Source code in qoolqit/graphs/base_graph.py
def min_distance(self, connected: bool | None = None) -> float:
    """Returns the minimum distance in the graph.

    Arguments:
        connected: if True/False, computes only over connected/disconnected nodes.
    """
    distance: float
    if connected is None:
        distance = min(self.distances(self.all_node_pairs).values())
    elif connected:
        distance = min(self.distances(self.sorted_edges).values())
    else:
        distance = min(self.distances(self.all_node_pairs - self.sorted_edges).values())
    return distance

random_er classmethod

random_er(
    n: int, p: float, seed: int | None = None
) -> DataGraph

Constructs an Erdős-Rényi random graph.

Parameters:

  • n (int) –

    number of nodes.

  • p (float) –

    probability that any two nodes connect.

  • seed (int | None, default: None ) –

    random seed.

Source code in qoolqit/graphs/data_graph.py
@classmethod
def random_er(cls, n: int, p: float, seed: int | None = None) -> DataGraph:
    """Constructs an Erdős-Rényi random graph.

    Arguments:
        n: number of nodes.
        p: probability that any two nodes connect.
        seed: random seed.
    """
    base_graph = nx.erdos_renyi_graph(n, p, seed)
    graph = DataGraph.from_nodes(list(base_graph.nodes))
    graph.add_edges_from(base_graph.edges)
    return graph

random_ud classmethod

random_ud(
    n: int, radius: float = 1.0, L: float | None = None
) -> DataGraph

Constructs a random unit-disk graph.

The nodes are sampled uniformly from a square of size (L x L). If L is not given, it is estimated based on a rough heuristic that of packing N nodes on a square of side L such that the expected minimum distance is R, leading to L ~ (R / 2) * sqrt(π * n).

Parameters:

  • n (int) –

    number of nodes.

  • radius (float, default: 1.0 ) –

    radius to use for defining the unit-disk edges.

  • L (float | None, default: None ) –

    size of the square on which to sample the node coordinates.

Source code in qoolqit/graphs/data_graph.py
@classmethod
def random_ud(
    cls,
    n: int,
    radius: float = 1.0,
    L: float | None = None,
) -> DataGraph:
    """Constructs a random unit-disk graph.

    The nodes are sampled uniformly from a square of size (L x L).
    If L is not given, it is estimated based on a rough heuristic that
    of packing N nodes on a square of side L such that the expected
    minimum distance is R, leading to L ~ (R / 2) * sqrt(π * n).

    Arguments:
        n: number of nodes.
        radius: radius to use for defining the unit-disk edges.
        L: size of the square on which to sample the node coordinates.
    """
    if L is None:
        L = (radius / 2) * ((np.pi * n) ** 0.5)
    coords = random_coords(n, L)
    graph = cls.from_coordinates(coords)
    edges = graph.ud_edges(radius)
    graph.add_edges_from(edges)
    return graph

rescale_coords

rescale_coords(
    *args: Any,
    scaling: float | None = None,
    spacing: float | None = None,
) -> None

Rescales the node coordinates by a factor.

Accepts either a scaling or a spacing factor.

Parameters:

  • scaling (float | None, default: None ) –

    value to scale by.

  • spacing (float | None, default: None ) –

    value to set as the minimum distance in the graph.

Source code in qoolqit/graphs/base_graph.py
def rescale_coords(
    self,
    *args: Any,
    scaling: float | None = None,
    spacing: float | None = None,
) -> None:
    """Rescales the node coordinates by a factor.

    Accepts either a scaling or a spacing factor.

    Arguments:
        scaling: value to scale by.
        spacing: value to set as the minimum distance in the graph.
    """
    if self.has_coords:
        msg = "Please pass either a `scaling` or a `spacing` value as a keyword argument."
        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)
        elif spacing is None and scaling is not None:
            self.coords = scale_coords(self.coords, scaling)
        else:
            raise TypeError(msg)
    else:
        raise AttributeError("Trying to rescale coordinates on a graph without coordinates.")

set_ud_edges

set_ud_edges(radius: float) -> None

Reset the set of edges to be equal to the set of unit-disk edges.

Source code in qoolqit/graphs/data_graph.py
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}

square classmethod

square(m: int, n: int, spacing: float = 1.0) -> DataGraph

Constructs a square lattice graph, with respective coordinates.

Parameters:

  • m (int) –

    Number of rows of square.

  • n (int) –

    Number of columns of square.

  • spacing (float, default: 1.0 ) –

    The distance between adjacent nodes on the final lattice.

Source code in qoolqit/graphs/data_graph.py
@classmethod
def square(
    cls,
    m: int,
    n: int,
    spacing: float = 1.0,
) -> DataGraph:
    """
    Constructs a square lattice graph, with respective coordinates.

    Arguments:
        m: Number of rows of square.
        n: Number of columns of square.
        spacing: The distance between adjacent nodes on the final lattice.
    """
    G = nx.grid_2d_graph(m, n)
    final_coords = [(x * spacing, y * spacing) for (x, y) in list(G.nodes)]
    G = nx.convert_node_labels_to_integers(G)

    graph = DataGraph.from_coordinates(final_coords)
    graph.add_edges_from(G.edges)
    return graph

to_matrix

to_matrix() -> NDArray[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:

  • NDArray[float64]

    Symmetric N x N matrix of dtype float64, where N is the number of nodes.

Source code in qoolqit/graphs/base_graph.py
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

to_pyg

to_pyg(
    node_attrs: Iterable[str] | None = None,
    edge_attrs: Iterable[str] | None = None,
    graph_attrs: Iterable[str] | None = None,
    node_weights_attr: str = "weight",
    edge_weights_attr: str = "edge_weight",
) -> Data

Convert the DataGraph to a PyTorch Geometric Data object.

Requires torch_geometric. Uses from_networkx internally.

Default attributes exported (if present on the graph):

  • Node "x"data.x; Edge "edge_attr"data.edge_attr
  • Graph "y"data.y

Use node_attrs, edge_attrs, graph_attrs for extras.

QoolQit internal dicts exported when populated:

  • coordsdata.pos (float64, shape (N, 2))
  • node_weightsdata.<node_weights_attr> (float64, shape (N,)). Defaults to "weight".
  • edge_weightsdata.<edge_weights_attr> (float64, shape (2*E,)). Defaults to "edge_weight".

Parameters:

  • node_attrs (Iterable[str] | None, default: None ) –

    extra node attributes to export (beyond x).

  • edge_attrs (Iterable[str] | None, default: None ) –

    extra edge attributes to export (beyond edge_attr).

  • graph_attrs (Iterable[str] | None, default: None ) –

    extra graph-level attributes to export (beyond y).

  • node_weights_attr (str, default: 'weight' ) –

    Data attribute name for node weights. Defaults to "weight".

  • edge_weights_attr (str, default: 'edge_weight' ) –

    Data attribute name for edge weights. Defaults to "edge_weight".

Returns:

  • Data

    PyTorch Geometric Data object.

Raises:

  • ImportError

    if torch_geometric is not installed.

Source code in qoolqit/graphs/data_graph.py
def to_pyg(
    self,
    node_attrs: Iterable[str] | None = None,
    edge_attrs: Iterable[str] | None = None,
    graph_attrs: Iterable[str] | None = None,
    node_weights_attr: str = "weight",
    edge_weights_attr: str = "edge_weight",
) -> torch_geometric.data.Data:
    """Convert the DataGraph to a PyTorch Geometric Data object.

    Requires ``torch_geometric``. Uses ``from_networkx`` internally.

    **Default attributes exported (if present on the graph):**

    - Node ``"x"`` → ``data.x``; Edge ``"edge_attr"`` → ``data.edge_attr``
    - Graph ``"y"`` → ``data.y``

    Use ``node_attrs``, ``edge_attrs``, ``graph_attrs`` for extras.

    **QoolQit internal dicts exported when populated:**

    - ``coords`` → ``data.pos`` (float64, shape ``(N, 2)``)
    - ``node_weights`` → ``data.<node_weights_attr>`` (float64, shape
      ``(N,)``). Defaults to ``"weight"``.
    - ``edge_weights`` → ``data.<edge_weights_attr>`` (float64, shape
      ``(2*E,)``). Defaults to ``"edge_weight"``.

    Arguments:
        node_attrs: extra node attributes to export (beyond x).
        edge_attrs: extra edge attributes to export (beyond edge_attr).
        graph_attrs: extra graph-level attributes to export (beyond y).
        node_weights_attr: Data attribute name for node weights.
            Defaults to ``"weight"``.
        edge_weights_attr: Data attribute name for edge weights.
            Defaults to ``"edge_weight"``.

    Returns:
        PyTorch Geometric Data object.

    Raises:
        ImportError: if ``torch_geometric`` is not installed.
    """
    try:
        import torch
        from torch_geometric.utils import from_networkx
    except ImportError as e:
        raise ImportError("Please, install the `torch_geometric` package.") from e

    node_attrs_set = set(node_attrs) if node_attrs else set()
    edge_attrs_set = set(edge_attrs) if edge_attrs else set()
    graph_attrs_list = list(graph_attrs) if graph_attrs else []

    # Add default PyG attributes if present in the graph
    if any("x" in d for _, d in self.nodes(data=True)):
        node_attrs_set.add("x")
    if any("edge_attr" in d for _, _, d in self.edges(data=True)):
        edge_attrs_set.add("edge_attr")
    if "y" in self.graph:
        graph_attrs_list.append("y")

    # Build a filtered copy with only the requested attributes
    filtered_graph = nx.Graph()
    filtered_graph.add_nodes_from(self.nodes())
    filtered_graph.add_edges_from(self.edges())

    for node, node_data in self.nodes(data=True):
        for key, value in node_data.items():
            if key in node_attrs_set:
                filtered_graph.nodes[node][key] = value
    for u, v, edge_data in self.edges(data=True):
        for key, value in edge_data.items():
            if key in edge_attrs_set:
                filtered_graph.edges[u, v][key] = value
    for attr in graph_attrs_list:
        if attr in self.graph:
            filtered_graph.graph[attr] = self.graph[attr]

    data = from_networkx(filtered_graph)

    # 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
    if self.has_node_weights:
        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)
    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]))
        setattr(data, edge_weights_attr, torch.tensor(edge_weights, dtype=torch.float64))

    return data

triangular classmethod

triangular(
    m: int, n: int, spacing: float = 1.0
) -> DataGraph

Constructs a triangular lattice graph, with respective coordinates.

Parameters:

  • m (int) –

    Number of rows of triangles.

  • n (int) –

    Number of columns of triangles.

  • spacing (float, default: 1.0 ) –

    The distance between adjacent nodes on the final lattice.

Source code in qoolqit/graphs/data_graph.py
@classmethod
def triangular(
    cls,
    m: int,
    n: int,
    spacing: float = 1.0,
) -> DataGraph:
    """
    Constructs a triangular lattice graph, with respective coordinates.

    Arguments:
        m: Number of rows of triangles.
        n: Number of columns of triangles.
        spacing: The distance between adjacent nodes on the final lattice.
    """
    G = nx.triangular_lattice_graph(m, n, with_positions=True)
    G = nx.convert_node_labels_to_integers(G)
    pos_unit = nx.get_node_attributes(G, "pos")

    final_pos = {node: (x * spacing, y * spacing) for node, (x, y) in pos_unit.items()}

    graph = cls.from_coordinates(final_pos)
    graph.add_edges_from(G.edges)
    return graph

ud_edges

ud_edges(radius: float, tol: float = 1e-07) -> set

Returns the set of edges whose distance is at most the given radius.

Parameters:

  • radius (float) –

    the unit-disk radius; a pair of nodes is included if their distance is at most this value.

  • tol (float, default: 1e-07 ) –

    numerical tolerance added to the radius to account for floating point error.

Source code in qoolqit/graphs/base_graph.py
def ud_edges(self, radius: float, tol: float = 1e-7) -> set:
    """Returns the set of edges whose distance is at most the given radius.

    Arguments:
        radius: the unit-disk radius; a pair of nodes is included if their distance is at
            most this value.
        tol: numerical tolerance added to the radius to account for floating point error.
    """
    if radius < 0:
        raise ValueError("`radius` must be a non-negative value.")
    if not self.has_coords:
        raise AttributeError(
            "Trying to compute unit-disk edges for a graph without coordinates."
        )

    return set(e for e, d in self.distances().items() if d <= radius + tol)

ud_radius_range

ud_radius_range() -> tuple

Return the range (R_min, R_max) where the graph is unit-disk.

The graph is unit-disk if the maximum distance between all connected nodes is smaller than the minimum distance between disconnected nodes. This means that for any value R in that interval, the following condition is true:

graph.ud_edges(radius = R) == graph.sorted edges

Source code in qoolqit/graphs/base_graph.py
def ud_radius_range(self) -> tuple:
    """Return the range (R_min, R_max) where the graph is unit-disk.

    The graph is unit-disk if the maximum distance between all connected nodes is
    smaller than the minimum distance between disconnected nodes. This means that
    for any value R in that interval, the following condition is true:

    graph.ud_edges(radius = R) == graph.sorted edges
    """
    if self.has_coords:
        n_edges = len(self.sorted_edges)
        if n_edges == 0:
            # If the graph is empty and has coordinates
            return (0.0, self.min_distance(connected=False))
        elif n_edges == len(self.all_node_pairs):
            # If the graph is fully connected
            return (self.max_distance(connected=True), float("inf"))
        elif self.max_distance(connected=True) < self.min_distance(connected=False):
            return (self.max_distance(connected=True), self.min_distance(connected=False))
        else:
            raise ValueError("Graph is not unit disk.")
    else:
        raise AttributeError("Checking if graph is unit disk is not valid without coordinates.")

all_node_pairs

all_node_pairs(nodes: Iterable) -> set

Return all pairs of nodes (u, v) where u < v.

Parameters:

  • nodes (Iterable) –

    set of node indices.

Source code in qoolqit/graphs/utils.py
def all_node_pairs(nodes: Iterable) -> set:
    """Return all pairs of nodes (u, v) where u < v.

    Arguments:
        nodes: set of node indices.
    """
    return set(filter(lambda x: x[0] < x[1], product(nodes, nodes)))

distances

distances(coords: dict, edge_list: Iterable) -> dict

Return a dictionary of edge distances.

Parameters:

  • coords (dict) –

    dictionary of node coordinates.

  • edge_list (Iterable) –

    edge list to compute the distances for.

Source code in qoolqit/graphs/utils.py
def distances(coords: dict, edge_list: Iterable) -> dict:
    """Return a dictionary of edge distances.

    Arguments:
        coords: dictionary of node coordinates.
        edge_list: edge list to compute the distances for.
    """
    return {edge: dist(coords[edge[0]], coords[edge[1]]) for edge in edge_list}

random_coords

random_coords(n: int, L: float = 1.0) -> list

Generate a random set of node coordinates on a square of side L.

Parameters:

  • n (int) –

    number of coordinate pairs to generate.

  • L (float, default: 1.0 ) –

    side of the square.

Source code in qoolqit/graphs/utils.py
def random_coords(n: int, L: float = 1.0) -> list:
    """Generate a random set of node coordinates on a square of side L.

    Arguments:
        n: number of coordinate pairs to generate.
        L: side of the square.
    """
    x_coords = np.random.uniform(low=-L / 2, high=L / 2, size=(n,)).tolist()
    y_coords = np.random.uniform(low=-L / 2, high=L / 2, size=(n,)).tolist()
    return [(x, y) for x, y in zip(x_coords, y_coords)]

random_edge_list

random_edge_list(nodes: Iterable, k: int) -> list

Generates a random set of k edges linkings items from a set of nodes.

Source code in qoolqit/graphs/utils.py
def random_edge_list(nodes: Iterable, k: int) -> list:
    """Generates a random set of k edges linkings items from a set of nodes."""
    all_edges = all_node_pairs(nodes)
    return random.sample(tuple(all_edges), k=k)

scale_coords

scale_coords(coords: dict, scaling: float) -> dict

Scale the coordinates by a given value.

Parameters:

  • coords (dict) –

    dictionary of node coordinates.

  • scaling (float) –

    value to scale by.

Source code in qoolqit/graphs/utils.py
def scale_coords(coords: dict, scaling: float) -> dict:
    """Scale the coordinates by a given value.

    Arguments:
        coords: dictionary of node coordinates.
        scaling: value to scale by.
    """
    scaled_coords = {i: (c[0] * scaling, c[1] * scaling) for i, c in coords.items()}
    return scaled_coords

space_coords

space_coords(coords: dict, spacing: float) -> dict

Spaces the coordinates so the minimum distance is equal to a set spacing.

Parameters:

  • coords (dict) –

    dictionary of node coordinates.

  • spacing (float) –

    value to set as minimum distance.

Source code in qoolqit/graphs/utils.py
def space_coords(coords: dict, spacing: float) -> dict:
    """Spaces the coordinates so the minimum distance is equal to a set spacing.

    Arguments:
        coords: dictionary of node coordinates.
        spacing: value to set as minimum distance.
    """
    pairs = all_node_pairs(list(coords.keys()))
    min_dist = min(distances(coords, pairs).values())
    scale_factor = spacing / min_dist
    return scale_coords(coords, scale_factor)