Exploring the Device Quantum Architecture

This notebook introduces the two types of quantum architecture, that are exposed by each device on VTT QX:

The notebook explains how to query each object and how they differ. We start by initializing the Qiskit client that we’ll use for our exploration.

Initializing the Qiskit client

To initialize the Qiskit client, copy your VTT QX API token into the line marked with TODO.

import os
from iqm.qiskit_iqm import IQMProvider

os.environ["IQM_TOKEN"] = "TODO: Add your VTT QX API token here"
qx_url = "https://qx.vtt.fi/api/devices/q50"

provider = IQMProvider(qx_url)
backend = provider.get_backend()

Static Quantum Architecture

In the picture below, taken from the VTT QX dashboard, the VTT Q50 device topology is visualized. We see that QB32 and its surrounding couplers, as well as the coupler QB10-QB18 are displayed in gray, indicating that no calibration information is available. For VTT Q50, those qubits and couplers have been deactivated and are not usable.

VTT Q50 device topology

We can fetch the same data used to generate the QPU visualization by manually querying the VTT QX API

import requests
from pprint import pprint

url = f"{qx_url}/quantum-architecture"
headers = {
    "Accept": "application/json",
    "Authorization": f"Bearer {os.environ['IQM_TOKEN']}",
}
r = requests.get(url, headers=headers)
quantum_architecture = r.json()["quantum_architecture"]

# pprint(quantum_architecture)

We can check the number of qubits:

print(f"Number of qubits: {len(quantum_architecture['qubits'])}")
Number of qubits: 54

which matches the number of qubits shown in the figure. For running quantum algorithms, it is generally not useful to include deactivated qubits as they must be filtered out during the transpilation procedure. Therefore, another representation of the quantum architecture exists that excludes deactivated qubits, the dynamic quantum architecure.

Dynamic Quantum Architecture

The dynamic quantum architecture, is generated after every calibration run, which, in theory, allows us to dynamically exclude qubits with low fidelity (this feature is currently not in use). The dynamic quantum architecture, is the default data that is fetched by client libraries such as Qiskit or Cirq to initialize the coupling map and learn about supported operations.

For example, when we query the Qiskit backend, we see that it is aware of the missing qubit, as it shows 53 instead of 54 qubits

print(f"Number of qubits: {backend.num_qubits}")
Number of qubits: 53

Next, we print the mapping of index to qubit name:

for i in range(backend.num_qubits):
    print(f"{i} --> {backend.index_to_qubit_name(i)}")

We see that while indices are continuous, there is a jump in the qubit naming between QB31 and QB33 as QB32 has been excluded. This means that for all qubits with index below 31, the corresponding name is QB{index + 1} and for qubits with index greater or equal than 31, the corresponding name is QB{index + 2}.

Note: A particular DQA is always tied to a calibration set ID. Therefore, the index to physical qubit mapping shown above might not apply to the calibration you are usng. To avoid confusion, it is recommended to use the backend function qubit_name_to_index when defining a fixed mapping for your quantum circuit. An example is shown below:

from qiskit import QuantumCircuit

qubits = ["QB24", "QB25", "QB33"]

qc = QuantumCircuit(len(qubits))

qc.h(0)
qc.cx(0, 1)
qc.cx(1, 2)
qc.measure_active()

qc.draw("mpl", idle_wires=False)

from qiskit import transpile

initial_layout = [backend.qubit_name_to_index(q) for q in qubits]
transpiled_circuit = transpile(
    qc, backend=backend, initial_layout=initial_layout, optimization_level=1
)

transpiled_circuit.draw("mpl", idle_wires=False)

After executing the quantum circuit, we can verify that the mapping has actually been applied

results = backend.run(transpiled_circuit, shots=1024).result()
pprint(results.request.circuits[0].instructions)
(Instruction(name='prx', implementation=None, qubits=('QB24',), args={'angle_t': 0.25000000000000006, 'phase_t': 0.75}),
 Instruction(name='prx', implementation=None, qubits=('QB25',), args={'angle_t': 0.25000000000000006, 'phase_t': 0.75}),
 Instruction(name='cz', implementation=None, qubits=('QB25', 'QB24'), args={}),
 Instruction(name='prx', implementation=None, qubits=('QB25',), args={'angle_t': 0.25000000000000006, 'phase_t': 1.25}),
 Instruction(name='prx', implementation=None, qubits=('QB33',), args={'angle_t': 0.25000000000000006, 'phase_t': 0.75}),
 Instruction(name='cz', implementation=None, qubits=('QB25', 'QB33'), args={}),
 Instruction(name='prx', implementation=None, qubits=('QB33',), args={'angle_t': 0.25000000000000006, 'phase_t': 1.25}),
 Instruction(name='barrier', implementation=None, qubits=('QB24', 'QB25', 'QB33'), args={}),
 Instruction(name='measure', implementation=None, qubits=('QB24',), args={'key': 'measure_3_0_0'}),
 Instruction(name='measure', implementation=None, qubits=('QB25',), args={'key': 'measure_3_0_1'}),
 Instruction(name='measure', implementation=None, qubits=('QB33',), args={'key': 'measure_3_0_2'}))

Alternatively, we can check the VTT QX job view to verify, that the mapping has been correctly applied

VTT QX job view showing the qubit mapping