Configuration and Usage#

This notebook describes the basic concepts and configuration of Pulla.

import os
from pprint import pprint
from IPython.core.display import HTML

from qiskit import QuantumCircuit, visualization
from qiskit.compiler import transpile

from iqm.qiskit_iqm import IQMProvider
from iqm.pulla.pulla import Pulla
from iqm.pulla.utils_qiskit import qiskit_to_pulla, sweep_job_to_qiskit
from iqm.pulse.playlist.visualisation.base import inspect_playlist

Basics#

Pulla#

A Pulla object is conceptually an IQM quantum computer client for connecting to the IQM server and constructing a circuit-to-pulse compiler. It consists of:

  • method for creating the compiler object

  • method for executing pulse-level instruction schedules (e.g. those created by the compiler)

A compiler object defines the specific circuit-to-pulse compilation logic. It consists of the following:

  • Information about the QC (chip topology, channel properties, etc.)

  • Set of implementations for each native operation, including user-defined implementations

  • Compilation stages, divided into circuit (and above)-level stages, pulse-level stages, finalization stages and return data post-processing stages

  • Set of available circuit-level quantum operations (“native operations”) (including user-defined operations)

  • Methods for manipulating the calibration, operations, and implementations

Pulla can construct a standard compiler equivalent to the one used by the server side (IQM Server). You can also construct a compiler manually.

To create an instance of Pulla, you need to provide the URL of IQM Server. Upon successful initialization, some configuration data is printed (the verbosity of such messages will be controlled by a debug level value).

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

pulla = Pulla(iqm_server_url)

Call get_standard_compiler() method to get an instance of Compiler. One of the duties of the compiler is to provide the calibration defining the QPU’s operating point. By default, the default calibration set is fetched from the server, but the user can customize the calibration fetching using any sequence of observation loading rules via get_standard_compiler(loading_rules=<my custom rules>)

# Define the standard compiler that loads the default calibration set as its initial operating point.
compiler = pulla.get_standard_compiler()

compiler.show_stages()

get_standard_compiler() fetches the default calibration set from the server. This network request takes a few moments.

It may also be possible that, due to human error, the default calibration set stored on the server is invalid (or incompatible with your version of Pulla or IQM Pulse). In that case get_standard_compiler() will fail.

Settings#

The compiler handles the operation point via a SettingNode object that contains the entire calibration (HW-settings, gate implementation calibration, compilation options, and auxiliary characterization information) in a single editable tree structure. The settings tree will include any circuit parameters of a parametrized circuit in case that is what we are compiling. For this reason, the settings are generated per a circuit batch. To study the general structure of the Compiler settings tree we can now generate it for an empty placeholder circuit.

# Generate the settings for an empty list of circuits
settings = compiler.get_settings(circuits=[])

# Print it in HTML -- click on the subnodes open them up in the visualisation
settings

The settings.controllers node contains the operating point settings for the HW controllers in the backend. This includes, for example, settings for qubit drive frequencies and qubit and coupler flux parking voltages. The controllers node contains further subnodes for each physical component of the QPU, under which one can find the controllers related to that component. Additionally a general options controller defines certain QPU-wide settings.

# Print the controllers node -- click on the subnodes to open them up
settings.controllers
# Further zoom into the drive node of QB1
settings.controllers.QB1.drive

The operation point can be modified by manipulating the settings values. For demonstration purposes, we do not set the drive frequency of QB1 to 4 MHz. It should be noted that running a quantum computation with a modified operation point will probably result in non-sense data, unless you know what you’re doing! (But don’t worry, it is safe in the sense that other users will not be affected by the change you made).

# Set the QB1 drive frequency to 4 MHz
settings.controllers.QB1.drive.frequency = 4e6

# Print the drive node again to confirm the changed value
settings.controllers.QB1.drive

What about the other subnodes in the settings tree?

The gates and gate_definitions nodes concern the quantum operation calibration and definitions. The gates node includes calibration data for each operation (gate) defined in the compiler and their physcial implementations. As an example, the phased rotation (PRX) gate is defined on each qubit and is used to perform most single-qubit operations such as X180, X90, Y180 and Hadamard. The calibration data for PRX for a single qubit consists of the duration of the associated microwave pulse, the I and Q pulse amplitudes and any additional parameters of the I and Q waveforms.

The gate_definitions node contains useful metadata for each operation and its implementations (for example the arity of the operation – how many locus components it acts upon – or whether the operation or its implementation is symmetric with respect to the order of its locus components). Most of this data is read only, meaning it cannot be changed like the drive frequency in the example above. However, an important customisable setting here is the default implementation information, which defines the implementation used for the quantum operation (in case the user does not manually specify another implementation in a compiler pass).

# Store the PRX default implementation name in a variable -- this is what we would be using to create PRX in circuits
default_prx = settings.gate_definitions.prx.default_implementation.value

# Print the associated calibration data for QB1
settings.gates.prx[default_prx].QB1
# SettingNode contains many utility methods. We could have fetched the same default implementation node by just calling:
default_prx_node = settings.get_gate_node_for_locus("prx", "QB1")

# Change a setting here (again, this would probably cause the gate to malfunction, but there is no need to worry about that for now)
default_prx_node.amplitude_i = 0.2

# Print the node and see the change taking effect
default_prx_node
# Change the default implementation of PRX
settings.gate_definitions.prx.default_implementation = "drag_gaussian_sx"

# Print the default QB1 PRX calibration node to see that it points to a different place this time (note that this other implementation is
# probably not calibrated yet, so we would have to provide the calibration data before using it). 
settings.get_gate_node_for_locus("prx", "QB1")

Of the final two top-level subnodes settings.stages contains settings for the compilation loop itself (more about that later), whereas settings.characterization contains auxiliary information about QPU components and gates operating on them (for example settings.characterization.model contains certain estimated qubit and coupler Hamiltonian parameters).

Compiling and running circuits#

Now that we have an idea how the operation point is defined and manipulated, we can use it to compile a circuit, send the job for execution and view the returned data.

NOTE: qiskit_to_pulla is a utility function that combines Pulla.get_standard_compiler() and transforming the Qiskit circuits into the IQM format in a single function call

backend = IQMProvider(iqm_server_url).get_backend()

# Define a Qiskit circuit that produces a 3-qubit GHZ state
qc = QuantumCircuit(3, 3)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
qc.measure_all()

# Transpile the circuit and convert it to the IQM representation
qc_transpiled = transpile(qc, backend=backend, layout_method='sabre', optimization_level=3)
circuits, compiler = qiskit_to_pulla(pulla, backend, qc_transpiled)

# Use the original settings into compile
job_definition, context = compiler.compile(circuits=circuits)

# Visualize the compiled pulse schedule per control channel
HTML(inspect_playlist(job_definition.sweep_definition.playlist, [0]))
# Send the job for execution and plot the results
job = pulla.submit_playlist(job_definition, context=context)
job.wait_for_completion()
qiskit_result = sweep_job_to_qiskit(job, shots=1000)

print(f"Qiskit result counts:\n{qiskit_result.get_counts()}\n")
visualization.plot_histogram(qiskit_result.get_counts())

Complex readout#

For the constant implementation of the measure operation, the readout type is controlled by the acquisition_type parameter, which is set to "threshold" by default. The full key in the calibration set dictionary is gates.measure_fidelity.constant.QUBIT.acquisition_type, where QUBIT is the physical qubit name.

qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()

backend = IQMProvider(iqm_server_url).get_backend()
qc_transpiled = transpile(qc, backend=backend, layout_method='sabre', optimization_level=3)
circuits, compiler = qiskit_to_pulla(pulla, backend, qc_transpiled)
settings = compiler.get_settings(circuits=circuits)

# set shots to 10 as we just want to see the complex results
settings.set_shots(10)

# Change the terminal measure acquisition type to 'complex' for all qubits in the chip
for qubit in compiler.chip_topology.qubits_sorted:
    settings.get_gate_node_for_locus("measure_fidelity", qubit).acquisition_type = "complex"

job_definition, context = compiler.compile(circuits=circuits, settings=settings)
job = pulla.submit_playlist(job_definition, context=context)
job.wait_for_completion()

# Pulla.submit_playlist() returns a PullaJob object.
# The measurements are obtained using PullaJob.result() after PullaJob.wait_for_completion().

print(f"Raw results:\n{job.result().circuit_measurement_results}\n")