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.
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, station_control_result_to_qiskit
# Create a Pulla object and a qiskit-iqm backend for accessing the quantum computer.
cocos_url = os.environ['PULLA_COCOS_URL'] # or set the URL directly here
station_control_url = os.environ['PULLA_STATION_CONTROL_URL'] # or set the URL directly here
p = Pulla(station_control_url)
provider = IQMProvider(cocos_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')
# 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.execute() returns a StationControlResult object; the measurements are in StationControlResult.result
# in addition, by default execute() prints the measurement results; disable it with verbose=False
response_data = p.execute(playlist, context, settings, verbose=False)
qiskit_result = station_control_result_to_qiskit(response_data, shots=shots, execution_options=context['options'])
print(f"Raw results:\n{response_data.result}\n")
print(f"Qiskit result counts:\n{qiskit_result.get_counts()}\n")
visualization.plot_histogram(qiskit_result.get_counts())
[12-11 11:20:53;I] Submitted sweep with ID: eafc0f4c-0cc1-43af-8c8c-0d663bd01892
[12-11 11:20:53;I] Created task in queue with ID: 39650a5e-5643-4b35-a719-930b4ee597aa
[12-11 11:20:53;I] Sweep link: http://ixion.qc.iqm.fi/station/sweeps/eafc0f4c-0cc1-43af-8c8c-0d663bd01892
[12-11 11:20:53;I] Task link: http://ixion.qc.iqm.fi/station/tasks/39650a5e-5643-4b35-a719-930b4ee597aa
[12-11 11:20:53;I] Waiting for the sweep to finish...
[12-11 11:20:58;I] Sweep status: SweepStatus.SUCCESS
Raw results:
[{'meas_3_1_2': [[0.0], [1.0], [1.0], [1.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], [1.0], [0.0], [1.0], [0.0], [1.0], [0.0], [0.0], [0.0], [1.0], [0.0], [1.0], [1.0], [1.0], [1.0], [0.0], [1.0], [0.0], [0.0], [1.0], [0.0], [0.0], [0.0], [1.0], [1.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], [0.0], [1.0], [1.0], [0.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [1.0], [1.0], [1.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [0.0], [1.0], [0.0], [0.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [1.0], [1.0], [1.0]], 'meas_3_1_1': [[0.0], [1.0], [1.0], [1.0], [1.0], [1.0], [1.0], [0.0], [1.0], [1.0], [0.0], [1.0], [0.0], [1.0], [0.0], [0.0], [1.0], [0.0], [1.0], [0.0], [1.0], [0.0], [0.0], [1.0], [1.0], [0.0], [0.0], [1.0], [1.0], [0.0], [0.0], [1.0], [0.0], [0.0], [1.0], [0.0], [0.0], [1.0], [1.0], [1.0], [0.0], [0.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [0.0], [1.0], [0.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [1.0], [0.0], [1.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [0.0], [1.0], [0.0], [0.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [1.0], [1.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [1.0], [1.0], [1.0]], 'meas_3_1_0': [[0.0], [1.0], [1.0], [1.0], [1.0], [1.0], [1.0], [0.0], [1.0], [1.0], [0.0], [0.0], [0.0], [1.0], [0.0], [0.0], [1.0], [0.0], [1.0], [0.0], [1.0], [0.0], [0.0], [1.0], [1.0], [0.0], [0.0], [1.0], [1.0], [0.0], [0.0], [1.0], [0.0], [0.0], [1.0], [0.0], [0.0], [1.0], [1.0], [1.0], [0.0], [0.0], [1.0], [1.0], [0.0], [0.0], [0.0], [1.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.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], [1.0], [0.0], [1.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [0.0], [1.0], [0.0], [0.0], [1.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [1.0], [1.0], [1.0], [0.0], [0.0], [0.0], [0.0], [0.0], [1.0], [1.0], [1.0], [1.0], [1.0]]}]
Qiskit result counts:
{'000': 45, '111': 40, '011': 6, '010': 2, '100': 5, '101': 1, '110': 1}
# The results should be comparable to a direct circuit execution through CoCoS.
print("Executing the same circuit via CoCoS...")
job = backend.run(qc_optimized, shots=shots)
print(f"Qiskit result counts from CoCoS:\n{job.result().get_counts()}")
visualization.plot_histogram(job.result().get_counts())
Executing the same circuit via CoCoS...
Qiskit result counts from CoCoS:
{'111': 32, '000': 45, '100': 6, '011': 6, '101': 3, '110': 3, '001': 2, '010': 3}
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:
Quantum architecture in
QuantumArchitectureSpecification
format of IQM ClientInstance of
Pulla
Instance of
Compiler
When IQMPullaBackend.run()
is called, the following steps are performed:
Given Qiskit circuits are converted to Pulla format using
qiskit_circuits_to_pulla()
.Circuits are compiled with the provided compiler using
Compiler.compile()
.Settings are generated with the provided compiler using
Compiler.build_settings()
.Circuits are executed on the station associated with the provided
Pulla
instance.Results are retrieved and converted into a
DummyJob
, partially compatible with QiskitJob
.
Working example below:
from iqm.iqm_client import QuantumArchitectureSpecification
from iqm.pulla.utils_qiskit import IQMPullaBackend
architecture = QuantumArchitectureSpecification(
name="Adonis",
operations={
"prx": [["QB1"], ["QB2"], ["QB3"], ["QB4"], ["QB5"]],
"cc_prx": [["QB1"], ["QB2"], ["QB3"], ["QB4"], ["QB5"]],
"cz": [["QB1", "QB3"], ["QB2", "QB3"], ["QB4", "QB3"], ["QB5", "QB3"]],
"measure": [["QB1"], ["QB2"], ["QB3"], ["QB4"], ["QB5"]],
"barrier": [],
},
qubits=["QB1", "QB2", "QB3", "QB4", "QB5"],
qubit_connectivity=[["QB1", "QB3"], ["QB2", "QB3"], ["QB3", "QB4"], ["QB3", "QB5"]],
)
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())
[12-11 11:21:05;I] Submitted sweep with ID: 40419d2d-c8b3-42de-b907-1c808bb400fe
[12-11 11:21:05;I] Created task in queue with ID: a040aac1-9878-4faf-8faf-83f60f121d5e
[12-11 11:21:05;I] Sweep link: http://ixion.qc.iqm.fi/station/sweeps/40419d2d-c8b3-42de-b907-1c808bb400fe
[12-11 11:21:05;I] Task link: http://ixion.qc.iqm.fi/station/tasks/a040aac1-9878-4faf-8faf-83f60f121d5e
[12-11 11:21:05;I] Waiting for the sweep to finish...
[12-11 11:21:09;I] Sweep status: SweepStatus.SUCCESS
Authentication#
If the remote station requires authentication:
Install Cortex CLI with
pip install iqm-cortex-cli
Run
cortex init
. The wizard will start and ask questions. Accepting defaults is ok.Wizard will ask for
Authentication server URL
. Usually, it’s of the formhttps://STATION_ROOT/auth
. E.g. if the Station Control URL is"https://abc.com/station/"
, then the auth server URL is"https://abc.com/auth/"
Run
cortex auth login
. You should see info like so:To use the tokens file with IQM Client or IQM Client-based software, set the environment variable: export IQM_TOKENS_FILE=/home/user/.cache/iqm-cortex-cli/tokens.json
Set the environment variable
IQM_TOKENS_FILE
accordingly in your Jupyter notebook and passget_token_callback
to the Pulla initialization call like so:import os from iqm.iqm_client.authentication import TokenManager os.environ["IQM_TOKENS_FILE"]="/home/user/.cache/iqm-cortex-cli/tokens.json" p = Pulla( station_control_url=station_control_url, get_token_callback=TokenManager().get_bearer_token, )
Setting IQM_TOKENS_FILE
env. variable enables authentication for CoCoS as well, so your Qiskit runs against the same station will work, too.
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]))