Which Qubits on the QPU Are Used?

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}")