Executor inputs and outputs
Package versions
The code on this page was developed using the following requirements. We recommend using these versions or newer.
qiskit[all]~=2.3.0
qiskit-ibm-runtime~=0.43.1
The Executor primitive is part of the directed execution model, which provides more flexibility when customizing an error mitigation workflow.
The inputs and outputs of the Executor primitive are very different from those of the Sampler and Estimator primitives. For example, instead of taking a list of PUBs as the input, Executor takes a QuantumProgram, which contains a list of QuantumProgramItem objects. These container classes give you more flexibility than a PUB, which is a simple tuple data structure.
Executor's output is a QuantumProgramResult, which is an iterable and contains one element for each input QuantumProgramItem.
Inputs: Quantum programs
As stated previously, the input to an Executor primitive is a QuantumProgram, which is an iterable of
QuantumProgramItem objects. These objects can be of two types:
CircuitItem, which typically stores a circuit and its parameter values (if any).SamplexItem, which typically stores the following:- A template circuit
- A samplex object, which is used to generate randomized sets of parameters at runtime (for example to perform twirling or inject noise)
- Arguments for the samplex, which might include parameter values for the original circuit
Each of these items represents a different task for Executor to perform.
Before you begin
Some of the code examples on this page use samplex, which is part of the Samplomatic package. Therefore, before running those code block, you must install Samplomatic, as shown in the following code block. For more information, see the Samplomatic documentation.
pip install samplomatic
# For visualization support, include the visualization dependencies.
# pip install samplomatic[vis]Example: Create a QuantumProgram with two different tasks
First initialize your quantum program, then append program items to it by using either append_circuit_item or append_samplex_item (if a samplex is present), as shown in the following examples.
The following cell initializes a QuantumProgram and specifies that it should run 1024 shots for every configuration of each item in the program.
Unlike with Sampler, a QuantumProgram takes only a single shot value. If you want a different shot value, you need a separate QuantumProgram, which would be a separate job.
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime.quantum_program import QuantumProgram
from qiskit_ibm_runtime import Executor, QiskitRuntimeService
from qiskit.circuit import Parameter, QuantumCircuit
import numpy as np
from samplomatic import build
from samplomatic.transpiler import generate_boxing_pass_manager
# Initialize an empty program
program = QuantumProgram(shots=1024)
# Initialize and transpile a 3-qubit quantum circuit with 2 parameters.
circuit = QuantumCircuit(3)
circuit.h(0)
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.rz(Parameter("theta"), 0)
circuit.rz(Parameter("phi"), 1)
# `measure_all` adds a 3-bit classical register named "meas"
circuit.measure_all()
# Choose the least busy backend
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
# Generate a preset pass manager
# This will be used to convert the abstract circuit to an
# equivalent Instruction Set Architecture (ISA) circuit.
preset_pass_manager = generate_preset_pass_manager(
backend=backend, optimization_level=0
)
# Transpile the circuit
isa_circuit = preset_pass_manager.run(circuit)Append a CircuitItem
Next, append the target circuit, which was transpiled according to the backend's instruction set architecture (ISA), to the QuantumProgram. Since this circuit has two parameters, we must also provide the parameter values (10 sets in this example). Running this CircuitItem is the first task that the program will perform.
# Append the transpiled circuit and an array
# containing 10 sets of parameter values to the program
program.append_circuit_item(
isa_circuit,
circuit_arguments=np.random.rand(
10, 2
), # 10 sets of parameter values and 2 parameters
)Append a SamplexItem
Circuit items are executed without any sort of randomization. On the contrary, samplex items let you specify how to randomize their content. The next cell uses the generate_boxing_pass_manager() function to group the circuit's gates and measurements into boxes and add a twirling annotation to each box. It then generates a template circuit and a samplex pair using the build() function.
Running this SamplexItem is the second task that the program will perform.
See the Samplomatic API documentation for full details about samplex and its arguments. See the Samplomatic Transpiler guide for information about using the generate_boxing_pass_manager() function.
# Transpile the circuit, additionally grouping gates and measurements into annotated boxes
preset_pass_manager = generate_preset_pass_manager(
backend=backend, optimization_level=0
)
# Use the boxing pass manager to group gates
# and measurements into boxes and add
# a`Twirl` annotation.
preset_pass_manager.post_scheduling = generate_boxing_pass_manager(
# Add gate twirling
enable_gates=True,
# Add measurement twirling
enable_measures=True,
)
boxed_circuit = preset_pass_manager.run(circuit)
# Build the template circuit and the samplex. The template circuit has parametric gates
# without fixed values and the samplex randomly generates the parameter
# values on the server side at runtime to perform twirling.
template_circuit, samplex = build(boxed_circuit)
# Determine what arguments are required by the samplex.
# Input the arguments in samplex_arguments.
print(samplex.inputs())Output:
TensorInterface(<
- 'parameter_values' <float64[2]>: Input parameter values to use during sampling.
>)
# Append the template circuit and samplex as a samplex item
program.append_samplex_item(
template_circuit,
samplex=samplex,
samplex_arguments={
# the arguments required by the samplex.sample method
"parameter_values": np.random.rand(10, 2),
},
shape=(28, 10), # 28 randomizations and 10 sets of parameter values
)# Initialize an Executor with the default options
executor = Executor(mode=backend)
# Submit the job
job = executor.run(program)
# Retrieve the result
result = job.result()Outputs
Executor's output is a QuantumProgramResult, which is an iterable. It contains one entry per input QuantumProgramItem in the same order as the input items. Each of these output items is a dictionary where the keys are strings that correspond the classical registers' names in the input circuits (among others), so you no longer need to memorize these names like you did with Sampler output. The dictionary values are of type np.ndarray.
The result for the previous example contains these items:
CircuitItem result
The first item contains the results of running the first task (a CircuitItem) in the program. It contains a single key, meas, which is the name of the classical register in the input circuit. The value of this key maps to an np.ndarray of shape (parameter sets, shots, register bits), which is (10, 1024, 3) for the above example.
The following code illustrates how to access this information:
# Access the results of the classical register of task #0, a CircuitItem
result_0 = result[0]["meas"]
print(f"Result shape: {result_0.shape}")Output:
Result shape: (10, 1024, 3)
SamplexItem result
The second item contains the results of running the second task (a SamplexItem) in the program. This item contains multiple keys. The meas key, which is the name of the input circuit's classical register, maps to that register's array of results. This array has the shape (randomizations, parameter sets, shots, classical bits), or (28, 10, 1024, 3) in this example. Additionally, the output contains a measurement_flips.meas key, which is the bit-flip corrections to undo the measurement twirling for the meas register. This output shape will be (28, 10, 1, 3) for our example because only one shot is required to perform the bit-flip.
# Access the results of the classical register of task #1
result_1 = result[1]["meas"]
print(f"Result shape: {result_1.shape}")
# Access the bit-flip corrections
flips_1 = result[1]["measurement_flips.meas"]
print(f"Bit-flip corrections shape: {flips_1.shape}")
# Undo the bit flips via classical XOR
unflipped_result_1 = result_1 ^ flips_1Output:
Result shape: (28, 10, 1024, 3)
Bit-flip corrections shape: (28, 10, 1, 3)
Next steps
- Explore examples that use Executor.
- Learn about the directed execution model.
- Understand Executor broadcasting.