Which Qubits on the QPU Are Used?#
We start by defining the problem with maxcut_generator
, the QAOA and training it. It’s a simple 3-regular maxcut and single-layer QAOA, so the training is easy. The details are not important in this notebook.
from iqm.applications.maxcut import maxcut_generator
from iqm.qaoa.qubo_qaoa import QUBOQAOA
problem_size = 10
my_maxcut_problem = next(maxcut_generator(n=problem_size, n_instances=1, graph_family="regular", d=3, seed = 1))
my_qaoa = QUBOQAOA(problem=my_maxcut_problem, num_layers=1, initial_angles=[0.1, 0.2])
my_qaoa.train()
Next, we define the backend we’re going to use. This is important here because the backend contains information about the QPU topology.
import os
from iqm.qiskit_iqm.iqm_provider import IQMProvider
API_TOKEN = os.environ.get("IQM_RESONANCE_API_TOKEN")
# Real garnet is used here, but it's only used to get the QPU topology.
SERVER_URL_CRYSTAL = "https://cocos.resonance.meetiqm.com/garnet"
iqm_backend = IQMProvider(SERVER_URL_CRYSTAL, token=API_TOKEN).get_backend()
Transpile the circuit using the SparseTranspiler
and fit it roughly onto the QPU.
from iqm.qaoa.circuits import transpiled_circuit
qc1 = transpiled_circuit(qaoa = my_qaoa, backend = iqm_backend, transpiler = "SparseTranspiler", seed = 1337)
Now we inspect which physical qubits the circuit has been placed onto.
import matplotlib.pyplot as plt
import networkx as nx
log_to_phys_dict = qc1.layout.final_layout.get_virtual_bits()
# "ancilla" is the default name of the quantum register added to increase the number of qubits to match the QPU
active_qubits = [log_to_phys_dict[qb] for qb in log_to_phys_dict if qb._register.name != "ancilla"]
# We import the function to change rustworkx graph to networkx graph
from iqm.qaoa.transpiler.rx_to_nx import rustworkx_to_networkx
coupling_map_nx = rustworkx_to_networkx(iqm_backend.coupling_map.graph).to_undirected()
pos = nx.kamada_kawai_layout(coupling_map_nx)
# Draw all nodes
nx.draw_networkx_nodes(coupling_map_nx, pos, node_color="lightgray", node_size=500)
# Highlight specific nodes
nx.draw_networkx_nodes(coupling_map_nx, pos, nodelist=active_qubits, node_color="orange", node_size=500)
# Draw edges and labels
nx.draw_networkx_edges(coupling_map_nx, pos)
nx.draw_networkx_labels(coupling_map_nx, pos)
plt.title("Used qubits")
plt.axis("off")
plt.show()
We can also explicitly specify which qubits we want the circuit placed onto. We do this by providing the transpiler with initial_layout
.
The initial layout needs to correspond to the logical qubits in the routed circuit, so we start with that. For that we need to create the routed circuit (using our sparse / greedy router) and then construct its interaction graph.
from iqm.qaoa.transpiler.quantum_hardware import CrystalQPUFromBackend
from iqm.qaoa.transpiler.sparse.greedy_router import greedy_router
qpu = CrystalQPUFromBackend(iqm_backend) # Extract the QPU topology from the backend.
routed = greedy_router(my_qaoa.bqm, qpu) # Perform the greedy routing on the QPU topology.
qc_sparse = routed.build_qiskit(my_qaoa.betas, my_qaoa.gammas) # Build a circuit from the routing.
int_graph = nx.Graph()
for qubit in qc_sparse.qubits:
int_graph.add_node(qubit._index) # We want nodes labelled by qubit indices (integers), not by qubits themselves.
for instruction, qargs, _ in qc_sparse.data:
if instruction.name in {"swap", "rzz"}: # Add all 2-qubit gates as edges in the graph
q1, q2 = qargs
int_graph.add_edge(q1._index, q2._index)
nx.draw(int_graph, with_labels=True, node_color="lightblue", node_size=1500, font_size=15)
plt.show()
We use the interaction graph with the node indices to inform the initial_layout
for the transpilation.
our_layout = [10, 15, 4, 9, 14, 18, 3, 8, 13, 17]
qc2 = transpiled_circuit(qaoa = my_qaoa,
backend = iqm_backend,
transpiler = "SparseTranspiler",
seed = 1337,
initial_layout = our_layout
)
log_to_phys_dict = qc2.layout.final_layout.get_virtual_bits()
# "ancilla" is the default name of the quantum register added to increase the number of qubits to match the QPU
active_qubits = [log_to_phys_dict[qb] for qb in log_to_phys_dict if qb._register.name != "ancilla"]
# We import the function to change rustworkx graph to networkx graph
from iqm.qaoa.transpiler.rx_to_nx import rustworkx_to_networkx
coupling_map_nx = rustworkx_to_networkx(iqm_backend.coupling_map.graph).to_undirected()
pos = nx.kamada_kawai_layout(coupling_map_nx)
# Draw all nodes
nx.draw_networkx_nodes(coupling_map_nx, pos, node_color="lightgray", node_size=500)
# Highlight specific nodes
nx.draw_networkx_nodes(coupling_map_nx, pos, nodelist=active_qubits, node_color="orange", node_size=500)
# Draw edges and labels
nx.draw_networkx_edges(coupling_map_nx, pos)
nx.draw_networkx_labels(coupling_map_nx, pos)
plt.title("Used qubits")
plt.axis("off")
plt.show()
Providing an initial_layout
that doesn’t satisfy the topology of the interaction graph gives an error.
try:
# The first logical qubit is assigned to physical qubit 6. The rest is the same as above
wrong_layout = [6, 15, 4, 9, 14, 18, 3, 8, 13, 17]
qc3 = transpiled_circuit(qaoa = my_qaoa,
backend = iqm_backend,
transpiler = "SparseTranspiler",
seed = 1337,
initial_layout = wrong_layout
)
except Exception as e:
print(f"An error occurred: {type(e).__name__} - {e}")