Embedding
In this section, you will learn how to:
- understand how problem graphs are mapped onto the analog Rydberg device through embedding,
- use QoolQit embedders to transform graphs into physically realizable configurations,
- test whether an embedding satisfies hardware constraints (e.g. unit-disk condition),
- convert embedded graphs into qubit registers.
The embedding problem
Section titled “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 , such that an object is embedded into an object . 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 embedderembedder = ConcreteEmbedder()
# Access information about the embedding algorithmembedder.info
# Access the configuration of the embedding algorithmembedder.config
# Change some value of the embedding configurationembedder.config.param = new_value
# Define some initial data objectdata = some_data_generator()
# Embed the data with the embedderembedded_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.
All the available embedders are listed in the section Available embedders.
For more details on the definition of custom embedders the reader can refer to Custom embedders.
Available embedders
Section titled “Available embedders”Interaction embedder
Section titled “Interaction embedder”Interaction embedding means to encode a matrix in the interaction term of the Rydberg analog model. For a matrix , the goal is to find the set of coordinates that minimize
where is the distance between qubits and . This requires the matrix 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.embedding import InteractionEmbedder
embedder = InteractionEmbedder()InteractionEmbedder:| Algorithm: interaction_embedding| Config: InteractionEmbedderConfig(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 (external).
print(embedder.info)-- Embedding algorithm 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 thematrix of values 1/(r_ij)^6 approximate the off-diagonal terms of the input matrix.
Check the documentation for scipy.minimize for more information about each parameter:https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html
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[[0.11042058 1.37787172 0.52223948 1.55573126 1.25464786 0.2243045 ] [1.37787172 1.14683062 1.40535334 1.0753044 1.21195107 1.46143112] [0.52223948 1.40535334 0.5516316 1.77704975 0.95381219 1.06186701] [1.55573126 1.0753044 1.77704975 1.35345901 0.93727039 0.95289755] [1.25464786 1.21195107 0.95381219 0.93727039 1.01704484 1.62739762] [0.2243045 1.46143112 1.06186701 0.95289755 1.62739762 1.06176601]]Finally, running the embedding we obtain a DataGraph with coordinates that can be easily converted to a Register of qubits.
from qoolqit import Register
embedded_graph = embedder.embed(matrix)register = Register.from_graph(embedded_graph)
fig1, ax1 = plt.subplots(figsize=(4,4), dpi=200)embedded_graph.draw(ax=ax1)register.draw()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.0233', '0.0242', '0.0251', '0.0310', '0.0650', '0.1189', '0.2051', '1.1225', '1.3019', '1.4835', '1.4874', '1.5236', '1.5354', '1.5825', '1.7621']['0.2243', '0.5222', '0.9373', '0.9529', '0.9538', '1.0619', '1.0753', '1.2120', '1.2546', '1.3779', '1.4054', '1.4614', '1.5557', '1.6274', '1.7770']Spring-layout embedder
Section titled “Spring-layout embedder”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.embedding import SpringLayoutEmbedder
embedder = SpringLayoutEmbedder()SpringLayoutEmbedder:| Algorithm: spring_layout_embedding| Config: SpringLayoutConfig(iterations=100, 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 algorithm docstring:
Force-directed embedding, wrapping `nx.spring_layout`.
Generates a new graph with the same nodes and edges as the original graph, but withnode coordinates set to embed edge weights into pairwise distances as wᵢⱼ=1/rᵢⱼ^6.
The positions are generated by `nx.spring_layout`.Since `nx.spring_layout` embeds edge weights as wᵢⱼ = (k/rᵢⱼ)^3, we set: - `k=1` and `scale=None` to disable global rescaling of positions. - rescale weights wᵢⱼ -> wᵢⱼ^1/2to achieve our target embedding wᵢⱼ=1/rᵢⱼ^6.
Check the documentation for `nx.spring_layout` for more information about each parameter:https://networkx.org/documentation/stable/reference/generated/networkx.drawing.layout.spring_layout.html
Args: graph: the graph to embed according to its edge weights. iterations: maximum number of iterations to take. threshold: Threshold for relative error in node position changes. The iteration stops if force-displacement is below this threshold. seed: random seed for reproducibility.
Returns: DataGraph: graph with the node coordinates set according to the spring-layout embedding.In this case, the 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 (external). The parameters can be directly changed in the config.
embedder.config.iterations = 100embedder.config.seed = 1
print(embedder)SpringLayoutEmbedder:| Algorithm: spring_layout_embedding| Config: SpringLayoutConfig(iterations=100, threshold=0.0001, seed=1)Finally, we can run the embedder with the embed method.
import matplotlib.pyplot as pltfrom qoolqit import DataGraph
graph_1 = DataGraph.random_er(n = 7, p = 0.3, seed = 3)embedded_graph_1 = embedder.embed(graph_1)
fig, axs = plt.subplots(1, 2, figsize=(8,4), dpi=200)graph_1.draw(ax=axs[0])embedded_graph_1.draw(ax=axs[1])Now, we can check if the resulting graph is a unit-disk graph
embedded_graph_1.is_ud_graph()TrueIn 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)
fig, axs = plt.subplots(1, 2, figsize=(8,4), dpi=200)graph_2.draw(ax=axs[0])embedded_graph_2.draw(ax=axs[1])embedded_graph_2.is_ud_graph()FalseHowever, 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 instantiate 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()Coming soon...
