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_qiskitSubmitting Jobs with Pulse-Level Access
This guide demonstrates how to submit quantum computing jobs with pulse-level access using the IQM Pulla library.
Overview
What this pulse-level access example do:
- We define quantum circuits at the gate level using Qiskit
- The circuit is transpiled and converted to pulse schedules
- Jobs are executed using the Pulla client
Requirement
This notebook requires a Python virtual environment with the following additional libraries installed
iqm-pulla>=12.0.0,<13.0.0Step 1: Import Python library dependencies
Step 2: Initialize the pulse-level client
Initialize both the Pulla client for pulse-level execution and the IQM client for retrieving results.
iqm_server_url = "https://qx.vtt.fi"
device_name = "demo" # TODO: Change device accordingly
os.environ["IQM_TOKEN"] = "" # TODO: Import your Token
p = Pulla(iqm_server_url, quantum_computer=device_name)
provider = IQMProvider(iqm_server_url, quantum_computer=device_name)
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")
Step 3: Define a quantum circuit
Define the quantum circuit you want to execute. In this example we create a 3-qubit GHZ state — a maximally entangled state — by applying a Hadamard gate followed by two CNOT gates. The circuit is drawn to visualise its structure before execution.
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")
Step 4: Transpile and optimise the circuit for the target device
The circuit must be transpiled to match the native gate set and qubit topology of the target device. Transpilation rewrites the circuit using only the gates the device supports (PRX and CZ) and inserts SWAP gates where the connectivity requires it. The optimize_single_qubit_gates pass then merges adjacent single-qubit gates to reduce the overall circuit depth.
# 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())Step 5: Convert to pulse schedule, submit, and retrieve results
qiskit_to_pulla converts the optimised Qiskit circuit into Pulla’s internal representation. The compiler then produces an instruction schedule playlist, the low-level pulse waveforms that will drive the hardware, along with a settings object that controls execution parameters such as shot count.
Pulla.submit_playlist() sends the playlist to the device and returns a SweepJob. After calling wait_for_completion(), sweep_job_to_qiskit() wraps the raw results in a standard Qiskit Result object so you can use familiar methods like get_counts() and plot_histogram().