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.
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.
-- 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.
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()
Now, we can check if the resulting graph is a unit-disk graph
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()
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()
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
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.
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.
-- 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.
[[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()
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']