Executor inputs and outputs
The inputs and output of the Executor primitive are very different from those of the Sampler and Estimator primitives. As part of the directed execution model, the Executor primitive helps provide more flexibility when customizing your error mitigation workflow. This primitive takes a QuantumProgram as input, and outputs a Qiskit Runtime job, which is then run on an IBM® quantum computer.
Inputs: Quantum programs
The input to an Executor primitive is a QuantumProgram, which is an iterable of a
qiskit_ibm_runtime.quantum_program.QuantumProgramItem. Each of these items represents a
different task for Executor to perform. Typically, each item owns one of the following options:
- A
qiskit.circuit.QuantumCircuitwith static, non-parametrized gates - A parametrized
qiskit.circuit.QuantumCircuitwith an array of parameter values - A parametrized
qiskit.circuit.QuantumCircuitwith asamplomatic.samplex.Samplexto generate randomized arrays of parameter values
Each of these items, including instructions to add them to a QuantumProgram, is explained in more detail below.
The following cell initializes a QuantumProgram and specifies that it should run 1024 shots for every configuration of each item in the program. Then it appends a version of the target circuit with set parameters, transpiled according to the backend's ISA.
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime.quantum_program import QuantumProgram
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 circuit
circuit = ...
...
isa_circuit = preset_pass_manager.run(circuit)
# Append the circuit to the program
program.append_circuit_item(isa_circuit)Next, append a second item that contains a parametrized qiskit.circuit.QuantumCircuit and an array containing 10 sets of parameter values. This amounts to a circuit task requiring a total of 10240 shots (1024 per set of parameter values).
# Append the circuit and the parameter values to the program
program.append_circuit_item(
parametrized_isa_circuit,
circuit_arguments=np.random.rand(10, 3), # 10 sets of parameter values
)Finally, append a parametrized qiskit.circuit.QuantumCircuit and a samplomatic.samplex.Samplex, which is responsible for generating randomized sets of parameters for the given circuit. As part of the samplomatic.samplex.Samplex arguments, 10 sets of parameters for the parametric gates in the original circuit are provided. The shape request argument requests an extension of the implicit shape defined by the samplomatic.samplex.Samplex arguments. In particular, by setting shape
to (2, 14, 10) we request to randomize each of the 10 sets of parameters 28 times, and to arrange the randomized parameter sets in an array of shape (2, 14, 10).
See the samplomatic API documentation for full details about samplomatic.samplex.Samplex and its arguments.
# Transpile the circuit, additionally grouping gates and measurements into annotated boxes
preset_pass_manager = generate_preset_pass_manager(
backend=backend, optimization_level=0
)
preset_pass_manager.post_scheduling = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
)
boxed_circuit = preset_pass_manager.run(circuit)
# Build the template and the samplex
template, samplex = build(boxed_circuit)
# Append the template and samplex as a samplex item
program.append_samplex_item(
template,
samplex=samplex,
samplex_arguments={
# the arguments required by the samplex.sample method
"parameter_values": np.random.rand(10, 3),
},
shape=(2, 14, 10),
)Outputs
Executor's output is a qiskit_ibm_runtime.quantum_program.QuantumProgramResult, which is an iterable. It contains one
item per circuit task, and the items are in the same order as those in the program. Each of
these items is a dictionary where the keys are strings and the values are of type np.ndarray, with elements of type bool.
In the previous example, the result contains three items:
Result item 0
The first item in result is the result of running the first task in the program, the circuit with static gates.
It contains a single key, 'meas', corresponding to the name of the input circuit's
classical register, mapped to the results collected for this
classical register. The information is stored in an np.ndarray of shape (shots, register bits), for the above example, (1024, 3).
The following code illustrates how to access this information:
# Access the results of the classical register of task #0
result_0 = result[0]["meas"]
print(f"Result shape: {result_0.shape}")Result item 1
The second item contains the results of running the second task in the program, the circuit with parametrized gates.
It contains a single key, 'meas', mapped to an np.ndarray of shape (shots, parameter sets, register bits), for the above example, (1024, 10, 3).
The following code illustrates how to access this information:
# Access the results of the classical register of task #1
result_1 = result[1]["meas"]
print(f"Result shape: {result_1.shape}")Result item 2
The third item contains the results of running the third task in the program. This item
contains multiple keys. In addition to the 'meas' key (mapped to the array of results for
that classical register), it contains 'measurement_flips.meas', the bit-flip corrections to undo
the measurement twirling for the 'meas' register, as follows:
# Access the results of the classical register of task #2
result_2 = result[2]["meas"]
print(f"Result shape: {result_2.shape}")
# Access the bit-flip corrections
flips_2 = result[2]["measurement_flips.meas"]
print(f"Bit-flip corrections: {result_2.flips_2}")
# Undo the bit flips via classical XOR
unflipped_result_2 = result_2 ^ flips_2Next steps
- Explore examples that use Executor.