Quick Start#

IQM Pulla is a client-side software which allows the user to control the generation and execution of pulse-level instruction schedules on a quantum computer. Within the existing IQM QCCSW stack, Pulla is somewhere between circuit-level execution and EXA-experiment. Namely, with pulse-level access the user can:

  • compile a quantum circuit (e.g. a Qiskit circuit) into an instruction schedule on the client side

  • access and modify the calibration data to be used for the circuit-to-schedule compilation

  • view and modify the default implementations of quantum gates

  • define custom implementations of quantum gates

  • define new composite gates out of native gates and set their calibration data

  • control the multi-step compilation procedure, and edit the intermediate data

  • use custom pulse shapes

This notebook contains a small meaningful example for a “quick start”. Refer to other chapters for more details on various aspects of Pulla.

Authentication#

IQM uses bearer token authentication to manage access to quantum computers. Get your personal token from the web dashboard and set the environment variable IQM_TOKEN accordingly in your Jupyter notebook:

import os

os.environ["IQM_TOKEN"]="<YOUR TOKEN HERE>"

p = Pulla(iqm_server_url)

Setting IQM_TOKEN env. variable enables authentication for IQM Client as well, so your Qiskit runs against the same station will work, too.

Alternatively, the token can be provided as argument token to :class:.Pulla constructor:

p = Pulla(iqm_server_url, token="<YOUR TOKEN HERE>")
import os
from qiskit import QuantumCircuit, visualization
from qiskit.compiler import transpile
from iqm.qiskit_iqm import IQMProvider
from iqm.qiskit_iqm.iqm_transpilation import optimize_single_qubit_gates
from iqm.pulla.pulla import Pulla
from iqm.pulla.utils_qiskit import qiskit_to_pulla, sweep_job_to_qiskit
# Create a Pulla object and a qiskit-iqm backend for accessing the quantum computer.
iqm_server_url = os.environ['PULLA_IQM_SERVER_URL']  # or set the URL directly here
os.environ["IQM_TOKEN"] = os.environ.get("IQM_TOKEN")  # or set the token directly here

p = Pulla(iqm_server_url)
provider = IQMProvider(iqm_server_url)
backend = provider.get_backend()
shots = 100

# Define a quantum circuit.
qc = QuantumCircuit(3, 3)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
qc.measure_all()

qc.draw(output='mpl')
_images/d57c695950a0e79f4b8e7fda69bbe88d4818122b7e9d4a4ba13283822aaf1f0b.png
qc_transpiled = transpile(qc, backend=backend, layout_method='sabre', optimization_level=3)
qc_optimized = optimize_single_qubit_gates(qc_transpiled)
qc_optimized.draw(output="mpl")
_images/401cd55d774204d345c6a1af06916d8b4f0ecc896ea83a16661eeff3da39559a.png
# Transpile the circuit using Qiskit, and then convert it into Pulla format.
qc_transpiled = transpile(qc, backend=backend, layout_method='sabre', optimization_level=3)
qc_optimized = optimize_single_qubit_gates(qc_transpiled)
circuits, compiler = qiskit_to_pulla(p, backend, qc_optimized)

# Compile the circuit into an instruction schedule playlist.
playlist, context = compiler.compile(circuits)
settings, context = compiler.build_settings(context, shots=shots)

# Pulla.submit_playlist() returns a SweepJob object.
# The measurements are obtained using SweepJob.result() after SweepJob.wait_for_completion().
job = p.submit_playlist(playlist, settings, context=context)
job.wait_for_completion()
qiskit_result = sweep_job_to_qiskit(job, shots=shots, execution_options=context['options'])

print(f"Raw results:\n{job.result()}\n")
print(f"Qiskit result counts:\n{qiskit_result.get_counts()}\n")
visualization.plot_histogram(qiskit_result.get_counts())
[04-17 10:06:00;I] Submitted sweep with ID: 4ca23dc8-3302-4fe5-965c-14d283d1cc62
[04-17 10:06:00;I] Created task in queue with ID: 5908e9a9-77e2-40cb-9bd0-e0f934bd4528
[04-17 10:06:00;I] Sweep link: http://varda.qc.iqm.fi/station/sweeps/4ca23dc8-3302-4fe5-965c-14d283d1cc62
[04-17 10:06:00;I] Task link: http://varda.qc.iqm.fi/station/tasks/5908e9a9-77e2-40cb-9bd0-e0f934bd4528
[04-17 10:06:00;I] Waiting for the sweep to finish...
[04-17 10:06:00;I] Celery task ID: 5908e9a9-77e2-40cb-9bd0-e0f934bd4528
[04-17 10:06:02;I] Sweep status: SweepStatus.SUCCESS
Raw results:
[{'meas_3_1_2': [[1.0], [0.0], [1.0], [1.0], [0.0], [0.0], [1.0], [1.0], [1.0], [0.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [1.0], [0.0], [0.0], [0.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [0.0], [1.0], [0.0], [1.0], [0.0], [0.0], [1.0], [0.0], [1.0], [0.0], [0.0], [1.0], [0.0], [1.0], [0.0], [0.0], [0.0], [1.0], [1.0], [1.0], [0.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [1.0], [1.0], [0.0], [0.0], [0.0], [1.0], [0.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [0.0], [0.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [0.0], [0.0], [1.0], [1.0]], 'meas_3_1_1': [[1.0], [0.0], [1.0], [1.0], [1.0], [0.0], [0.0], [1.0], [1.0], [0.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [0.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [0.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [0.0], [1.0], [1.0], [0.0], [0.0], [0.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [1.0], [1.0], [0.0], [0.0], [1.0], [0.0], [0.0], [1.0], [1.0], [1.0], [0.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [0.0], [0.0], [1.0], [1.0], [0.0], [1.0], [1.0], [0.0], [1.0], [1.0], [0.0], [0.0], [0.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.0]], 'meas_3_1_0': [[1.0], [0.0], [1.0], [1.0], [0.0], [0.0], [1.0], [1.0], [1.0], [0.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [1.0], [0.0], [0.0], [0.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [0.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [1.0], [0.0], [0.0], [1.0], [0.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [0.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [0.0], [0.0], [1.0], [0.0], [1.0], [1.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.0], [0.0], [1.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [1.0], [0.0], [0.0], [1.0], [0.0], [0.0], [0.0], [1.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [0.0], [0.0], [1.0], [0.0]]}]

Qiskit result counts:
{'111': 26, '000': 49, '010': 9, '101': 4, '100': 4, '001': 3, '011': 1, '110': 4}
_images/946f7d64c21e9419c23aba43ccfd6ac4bcf332d7cae3a34546ef6bec6b71a8a3.png
# The results should be comparable to a direct circuit execution through IQM Server.
print("Executing the same circuit via IQM Server...")
job = backend.run(qc_optimized, shots=shots)
print(f"Qiskit result counts from IQM Server:\n{job.result().get_counts()}")

visualization.plot_histogram(job.result().get_counts())
Executing the same circuit via CoCoS...
Qiskit result counts from CoCoS:
{'100': 12, '000': 45, '110': 6, '111': 16, '010': 7, '011': 4, '101': 8, '001': 2}
_images/378bfc441e31397949884c365d21af5d6aa62cd28b7152aa224bcf6e43829e99.png

Pulla Qiskit Backend#

Pulla provides a Qiskit backend, with limited functionality. Its main purpose is to replace the normal execution of run(), which submits circuits to the remote server, with a local compilation and submission of pulse schedules to the remote server.

IQMPullaBackend does not provide any new functionality, but rather packs existing features and actions behind an illusion of using a normal Qiskit backend. You can perform all of the actions of IQMPullaBackend manually, but you may choose to use IQMPullaBackend in these cases:

  • You don’t need to control compilation, and want to use Pulla in the same way as a remote circuit-executing IQM Server uses it.

  • You want to run some existing apps written for Qiskit, e.g. benchmarking tools; they often build on top ot the circuit abstraction, and don’t necessarily give you easy access to the circuits, which makes it harder to use Pulla normally.

  • You don’t have access to a remote circuit-executing IQM Server, only to a pulse-executing IQM Server.

  • You don’t have any remote servers at all; all of the quantum control software is running locally (relevant for niche research cases)

To initialize an IQMPullaBackend instance, provide 3 arguments:

  1. Quantum architecture in DynamicQuantumArchitecture format of IQM Client

  2. Instance of Pulla

  3. Instance of Compiler

When IQMPullaBackend.run() is called, the following steps are performed:

  1. Given Qiskit circuits are converted to Pulla format using qiskit_circuits_to_pulla().

  2. Circuits are compiled with the provided compiler using Compiler.compile().

  3. Settings are generated with the provided compiler using Compiler.build_settings().

  4. Circuits are executed on the station associated with the provided Pulla instance.

  5. Results are retrieved and converted into a DummyJob, partially compatible with Qiskit Job.

Working example below:

from iqm.pulla.utils_qiskit import IQMPullaBackend
from iqm.iqm_client import IQMClient, DynamicQuantumArchitecture

client = IQMClient(iqm_server_url)
architecture: DynamicQuantumArchitecture = client.get_dynamic_quantum_architecture()

compiler = p.get_standard_compiler()
backend = IQMPullaBackend(architecture, p, compiler)

qc_transpiled = transpile(qc, backend=backend, layout_method='sabre', optimization_level=3)
qc_optimized = optimize_single_qubit_gates(qc_transpiled)

job = backend.run(qc_optimized, shots=shots)

visualization.plot_histogram(job.result().get_counts())
[04-17 10:07:08;I] Submitted sweep with ID: 95370d5a-f27f-4269-80a0-6a7108ebd387
[04-17 10:07:08;I] Created task in queue with ID: 057662b7-d621-460c-88ad-74efbd193bdb
[04-17 10:07:08;I] Sweep link: http://varda.qc.iqm.fi/station/sweeps/95370d5a-f27f-4269-80a0-6a7108ebd387
[04-17 10:07:08;I] Task link: http://varda.qc.iqm.fi/station/tasks/057662b7-d621-460c-88ad-74efbd193bdb
[04-17 10:07:08;I] Waiting for the sweep to finish...
[04-17 10:07:09;I] Celery task ID: 057662b7-d621-460c-88ad-74efbd193bdb
[04-17 10:07:12;I] Sweep status: SweepStatus.SUCCESS
_images/068f5ff0843ed91d8a6dcd1be2fc6839fecdcc60653303c55215854208900c1c.png

Using Pulla with IQM Resonance#

Pulla can also be used with IQM Resonance cloud service. You need to set the IQM Server URL to point to Resonance and specify the quantum computer to use.

iqm_server_url = os.environ.get("RESONANCE_URL")  # typically "https://resonance.meetiqm.com"
# The names of the available quantum computers can be found on the Resonance dashboard.
quantum_computer = os.environ.get("RESONANCE_QC_NAME")
os.environ["IQM_TOKEN"] = os.environ.get("RESONANCE_API_TOKEN")  # or set the token directly here

p = Pulla(iqm_server_url, quantum_computer=quantum_computer)
provider = IQMProvider(iqm_server_url, quantum_computer=quantum_computer)
backend = provider.get_backend(use_metrics=False)

# From now on, the usage is same as for non-Resonance usage
# qc = ...

qc_transpiled = transpile(qc, backend=backend, layout_method='sabre', optimization_level=3)
qc_optimized = optimize_single_qubit_gates(qc_transpiled)
circuits, compiler = qiskit_to_pulla(p, backend, qc_optimized)
pl, context = compiler.compile(circuits)
settings, context = compiler.build_settings(context, shots=shots)

job = p.submit_playlist(pl, settings, context=context)
job.wait_for_completion()
qiskit_result = sweep_job_to_qiskit(job, shots=shots, execution_options=context['options'])

print(f"Qiskit result counts:\n{qiskit_result.get_counts()}\n")
[04-02 15:52:50;I] Submitted sweep with ID: 0195f68f-bd5e-7731-abf8-bb48bd615be8
[04-02 15:52:50;I] Created task in queue with ID: 0195f68f-bd5e-7731-abf8-bb48bd615be8
[04-02 15:52:50;I] Waiting for the sweep to finish...
[04-02 15:53:01;I] Sweep status: SweepStatus.SUCCESS
Qiskit result counts:
{'111': 21, '100': 20, '000': 33, '011': 26}

Schedule visualization#

IQM Pulse comes with a schedule visualizer. It takes a Playlist (i.e. a compressed list of instruction schedules) and a list of schedule/segment indices to inspect. The playlist variable below is the one which we derived from the original Qiskit circuit using the Pulla compiler, and it only has one schedule.

from iqm.pulse.playlist.visualisation.base import inspect_playlist
from IPython.core.display import HTML

HTML(inspect_playlist(playlist, [0]))