Skip to content

The embedding problem

Embedding data and problems into the Rydberg analog model is a broad research topic. Typically, an embedding is a structure preserving map \(f_\text{embedding}: X \rightarrow Y\), such that an object \(X\) is embedded into an object \(Y\). Our goal is to define optimal embedding functions such that problem-specific data and definitions are embedded into model-compatible objects with the Rydberg analog model.

In QoolQit, all concrete embedders follow a basic interface set by the BaseEmbedder abstract base class:

from qoolqit import ConcreteEmbedder

# Initialize the embedder
embedder = ConcreteEmbedder()

# Access information about the embedding algorithm
embedder.info

# Access the configuration of the embedding algorithm
embedder.config

# Change some value of the embedding configuration
embedder.config.param = new_value

# Define some initial data object
data = some_data_generator()

# Embed the data with the embedder
embedded_data = embedder.embed(data)

In this case, ConcreteEmbedder exemplifies an embedder that already has a mapping function and the respective configuration dataclass for that mapping function. Below, we will exemplify how to use some of the pre-defined concrete embedders directly available in QoolQit, and then show some considerations when defining custom embedders.

Unit-disk graph embedding

Unit-disk graph embedding is the problem of finding a suitable function \(f_\text{UD}: V\rightarrow\mathbb{R}^2\) that assigns coordinates to each node in the graph, such that the resulting unit-disk graph exactly matches the original graph (see the graphs page for the definition of a unit-disk graph). In general, not all graphs can be embedded into unit-disk graphs, and deciding if a graph can be embedded into a unit-disk graph is a NP-Hard problem.

However, several heuristic algorithms can be developed to tackle the unit-disk graph embedding problem.

Spring-layout embedding

The spring-layout embedding utilizes the Fruchterman-Reingold force-directed algorithm. It assigns spring-like forces to the edges that keep nodes closer, while treating nodes themselves as repelling objects. The system is then simulated until the nodes find an equilibrium position, which represent the final coordinates assigned to the nodes.

In QoolQit, the SpringLayoutEmbedder directly wraps the nx.spring_layout function, and it maps a DataGraph without coordinates to another DataGraph with coordinates.

from qoolqit import SpringLayoutEmbedder

embedder = SpringLayoutEmbedder()
SpringLayoutEmbedder:
| Algorithm: spring_layout_embedding
| Config: SpringLayoutConfig(k=None, iterations=50, threshold=0.0001, seed=None)

As you can see above it holds an algorithm and a config with a set of default parameters. For information on the algorithm and parameters, you can use the embedder.info property.

print(embedder.info)
-- Embedding agorithm docstring:

Force-directed embedding, wrapping nx.spring_layout.

Generates a graph with the same nodes and edges as the original graph, but with
node coordinates set to be the positions given by nx.spring_layout.

Check the documentation for nx.spring_layout for more information on each parameter.

Arguments:
    graph: the graph to embed.
    k: optimal distance between nodes.
    iterations: maximum number of iterations to take.
    threshold: threshold value for relative error in node position changes.
    sed: random seed.

In this case, this embedder is a direct wrapper on top of nx.spring_layout, and any parameters are the ones directly used by that function. For more information, you can check the documentation for NetworkX. The parameters can be directly changed in the config.

embedder.config.iterations = 100
embedder.config.seed = 1

print(embedder)
SpringLayoutEmbedder:
| Algorithm: spring_layout_embedding
| Config: SpringLayoutConfig(k=None, iterations=100, threshold=0.0001, seed=1)

Finally, we can run the embedder with the embed method.

from qoolqit import DataGraph

graph_1 = DataGraph.random_er(n = 7, p = 0.3, seed = 3)

embedded_graph_1 = embedder.embed(graph_1)

graph_1.draw()
embedded_graph_1.draw()
2025-06-26T15:13:22.711521 image/svg+xml Matplotlib v3.10.3, https://matplotlib.org/ 2025-06-26T15:13:22.736466 image/svg+xml Matplotlib v3.10.3, https://matplotlib.org/

Now, we can check if the resulting graph is a unit-disk graph

embedded_graph_1.is_ud_graph()
True

In this case the embedding was successful and we obtained a unit-disk graph. For more densely connected graphs, the spring layout algorithm tends to struggle with finding a unit-disk graph embedding, if it even exists.

graph_2 = DataGraph.random_er(n = 7, p = 0.8, seed = 3)

embedded_graph_2 = embedder.embed(graph_2)

graph_2.draw()
embedded_graph_2.draw()
2025-06-26T15:13:22.845835 image/svg+xml Matplotlib v3.10.3, https://matplotlib.org/ 2025-06-26T15:13:22.872986 image/svg+xml Matplotlib v3.10.3, https://matplotlib.org/

embedded_graph_2.is_ud_graph()
False
While the algorithm converged and assigned positions to each node, the resulting embedded graph fails the unit-disk graph test.

However, in both cases, we have embedded the original data into a graph with coordinates, which is an object that is compatible with the Rydberg analog model. As such, we can directly intantiate a register of qubits from these graphs.

from qoolqit import Register

register_1 = Register.from_graph(embedded_graph_1)
register_2 = Register.from_graph(embedded_graph_2)

register_1.draw()
register_2.draw()
2025-06-26T15:13:22.962296 image/svg+xml Matplotlib v3.10.3, https://matplotlib.org/ 2025-06-26T15:13:23.011643 image/svg+xml Matplotlib v3.10.3, https://matplotlib.org/

Matrix embedding

Matrix embedding is the problem of encoding a matrix into the Rydberg analog model. Several approaches can be taken, using different algorithms.

Interaction embedding

Interaction embedding means to encode a matrix in the interaction term of the Rydberg analog model. For a matrix \(U\), the goal is to find the set of coordinates that minimize

\[ \min_{\{(x,\, y)\}}~\sum_{ij}\left\|U_{ij}-\frac{1}{r^6_{ij}}\right\|, \]

where \(r_{ij} = \sqrt{(x_i-x_j)^2 + (y_i-y_j)^2}\) is the distance between qubits \(i\) and \(j\). This requires the matrix \(U\) to be positive and symmetric, and only the off-diagonal terms are embedded.

In QoolQit, the InteractionEmbedder performs this minimization using scipy.minimize, and it maps a np.ndarray to a DataGraph with coordinates.

from qoolqit import InteractionEmbedder

embedder = InteractionEmbedder()
InteractionEmbedder:
| Algorithm: interaction_embedding
| Config: InteractionEmbeddingConfig(method='Nelder-Mead', maxiter=200000, tol=1e-08)

Checking the info on the embedder, we see a few parameters are available for customization through the config. There are parameters that get passed to scipy.minimize, and their description can be found in the SciPy documentation.

print(embedder.info)
-- Embedding agorithm docstring:

Matrix embedding into the interaction term of the Rydberg Analog Model.

Uses scipy.minimize to find the optimal set of node coordinates such that the
matrix of values 1/(r_ij)^6 approximate the off-diagonal terms of the input matrix.

Check scipy.minimize documentation for more information on each parameter.

Arguments:
    matrix: the matrix to embed.
    method: the method used by scipy.minimize.
    maxiter: maximum number of iterations.
    tol: tolerance for termination.

We can try it out with the default configuration by generating a random symmetric positive matrix.

import numpy as np

matrix = np.random.rand(6, 6)

matrix = matrix + matrix.T
[[1.81810399 1.40652463 0.92958593 1.7192608  0.83303631 1.23224232]
 [1.40652463 0.12633379 1.23072871 0.91915965 0.88104688 1.11413614]
 [0.92958593 1.23072871 0.45092508 1.07970488 0.30592813 1.19805354]
 [1.7192608  0.91915965 1.07970488 0.01076373 0.95109728 0.70441655]
 [0.83303631 0.88104688 0.30592813 0.95109728 0.76207888 1.70788061]
 [1.23224232 1.11413614 1.19805354 0.70441655 1.70788061 0.41831809]]

Finally, running the embedding we obtain a DataGraph with coordinates that can be easily converted to a Register of qubits.

import numpy as np

embedded_graph = embedder.embed(matrix)

register = Register.from_graph(embedded_graph)

embedded_graph.draw()
register.draw()
2025-06-26T15:13:23.258943 image/svg+xml Matplotlib v3.10.3, https://matplotlib.org/ 2025-06-26T15:13:23.307409 image/svg+xml Matplotlib v3.10.3, https://matplotlib.org/

To check how the embedding performed, we can inspect the interaction values in the Register and compare them to the off-diagonal elements in the matrix.

interactions = list(register.interactions().values())

triang_upper = np.triu(matrix, k = 1)
off_diagonal = triang_upper[triang_upper != 0].tolist()

print([f"{f:.4f}" for f in sorted(interactions)])
print([f"{f:.4f}" for f in sorted(off_diagonal)])
['0.0041', '0.0166', '0.0231', '0.0389', '0.0500', '0.0579', '0.9001', '0.9242', '1.0957', '1.1148', '1.2573', '1.2618', '1.4188', '1.7158', '1.7345']
['0.3059', '0.7044', '0.8330', '0.8810', '0.9192', '0.9296', '0.9511', '1.0797', '1.1141', '1.1981', '1.2307', '1.2322', '1.4065', '1.7079', '1.7193']