Skip to main content
IBM Quantum Platform

Executor inputs and outputs

  • The code on this page was developed using the following requirements. We recommend using these versions or newer.

    qiskit[all]~=2.6.1
    qiskit-ibm-runtime~=0.50.0
    samplomatic~=0.21.0
    

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.

Note

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)

The right-most axis of result_0 runs over classical bits in little-endian order. Thus, index 0 holds the outcome of classical bit 0, the least-significant bit.

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.

Note that the right-most axis of result_1 runs over classical bits in little-endian order.

# 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_1

Output:

Result shape: (28, 10, 1024, 3)
Bit-flip corrections shape: (28, 10, 1, 3)

View execution spans

A single Executor.run() call submits a single job. On the server side, however, the service does not necessarily run every configuration of the quantum program at the same time. Instead, it groups the program items' bound circuits into chunks, and runs one chunk at a time on the QPU. You don't need to split your program or submit additional jobs; the chunking happens automatically within the one job. The result's metadata attribute contains timing information about these chunks in result.metadata.chunk_timing. These timestamps are the actual start and stop times of each chunk on the QPU. You can use this information to see when particular data was collected on the QPU, to understand how Executor distributed the work, or to diagnose performance issues.

chunk_timing is a list that contains one ChunkSpan per executed chunk. Each ChunkSpan has the following attributes:

  • start and stop: The UTC timestamps when the chunk started and stopped executing on the QPU.
  • parts: A list of ChunkPart objects that describe which program items contributed to the chunk. Each ChunkPart contains the following:
    • idx_item: The index of the program item, in the order that the items were appended to the QuantumProgram.
    • size: The number of configurations (elements of the item's shape) from that item that were executed in this chunk. For example, the SamplexItem in the previous example has shape (28, 10), so it has 280 configurations in total.

The following code prints the duration of each chunk and the contents of each chunk for the job submitted previously:

for idx, span in enumerate(result.metadata.chunk_timing):
    duration = (span.stop - span.start).total_seconds()
    print(f"Chunk {idx}: {span.start} to {span.stop} ({duration:.3f} s)")
    for part in span.parts:
        print(f"  Item {part.idx_item}: {part.size} configurations")

Output:

Chunk 0: 2026-11-06 01:47:46.009718+00:00 to 2026-11-06 01:49:05.577720+00:00 (79.568 s)
  Item 0: 10 configurations
  Item 1: 280 configurations

The QuantumProgramResult.timing property wraps the same list in a ChunkTiming object, which also reports global information about the collection of chunks:

timing = result.timing

print("Number of chunks:", len(timing))
print("   Start of the first chunk:", timing.start)
print("      End of the last chunk:", timing.stop)
print("        Total duration (s):", timing.duration)

Output:

Number of chunks: 1
   Start of the first chunk: 2026-11-06 01:47:46.009718+00:00
      End of the last chunk: 2026-11-06 01:49:05.577720+00:00
        Total duration (s): 79.568002

Visualize execution spans

Use the draw_chunk_timings function to visualize the chunk timings on an interactive bar plot. The x-axis is time, and each chunk is drawn as a horizontal segment that spans its start and stop times. The y-axis shows the cumulative number of configurations completed (or the percentage of the total workload, if you set normalize_y=True). Hover over a segment to see the timestamps, duration, and parts of that chunk.

The function returns a plotly figure, which you can display directly, save to a file, or both. It accepts either the chunk_timing list or a ChunkTiming object. You can pass several of them, for example from different jobs, to draw them on the same plot. Set common_start=True to align them at a common start time for comparison.

Plotly required

draw_chunk_timings requires plotly. You can install it by running pip install plotly.

Gaps between chunks

The visualization might show gaps between the chunk execution. All jobs submitted to a real QPU go through some classical preprocessing, such as translation into pulses that can be sent to the hardware. Because this preprocessing can be done in parallel, the service dequeues and starts multiple jobs simultaneously. The QPU, however, can only process one unit of work at a time. Therefore, if a job generates multiple chunks, its chunks might be interleaved with those of other users' jobs, and each chunk might have to wait for its turn on the QPU. This is why gaps can appear between chunks.

from qiskit_ibm_runtime.visualization import draw_chunk_timings

fig = draw_chunk_timings(result.timing, names="Executor job")

# Alternatively, call the `draw()` method of the ChunkTiming object
# fig = result.timing.draw()

# Uncomment the following line to display the figure
# fig.show(renderer="notebook")

# Save to a file
# fig.write_html("chunk_timings.html")

Next steps

Recommendations
Was this page helpful?
Report a bug, typo, or request content on GitHub.