Zum Hauptinhalt springen
Dieses Projekt befindet sich in der Beta-Phase. Einen Fehler gefunden? GitHub Issue erstellen. Frage oder Idee? In Discussions fragen — wir freuen uns über dein Feedback!
Wie ist die Übersetzungsqualität auf dieser Seite?

Sampler-Eingaben und -Ausgaben

Paketversionen

Der Code auf dieser Seite wurde mit den folgenden Anforderungen entwickelt. Wir empfehlen, diese oder neuere Versionen zu verwenden.

qiskit[all]~=2.5.2
qiskit-ibm-runtime~=0.47.0

Diese Seite gibt einen Überblick über die Eingaben und Ausgaben des qiskit-ibm-runtime Sampler-Primitive, das Workloads auf dem IBM Quantum® Compute Service ausführt. Sampler ermöglicht es dir, vektorisierte Workloads effizient zu definieren, indem du eine Datenstruktur verwendest, die als Primitive Unified Bloc (PUB) bekannt ist. Diese werden als Eingaben für die run()-Methode des Sampler-Primitive verwendet, die den definierten Workload als Job ausführt. Nachdem der Job abgeschlossen ist, werden die Ergebnisse in einem Format zurückgegeben, das sowohl von den verwendeten PUBs als auch von den für das Primitive festgelegten Laufzeitoptionen abhängt.

Eingaben​

Jedes PUB hat das folgende Format:

(<single circuit>, <one or more optional parameter value>, <optional shots>),

Es kann mehrere parameter values-Elemente geben, und jedes Element kann entweder ein Array oder ein einzelner Parameter sein, je nach dem gewählten Circuit. Außerdem muss die Eingabe Messungen enthalten.

Für das Sampler-Primitive kann ein PUB höchstens drei Werte enthalten:

  • Einen einzelnen QuantumCircuit, der ein oder mehrere Parameter-Objekte enthalten kann Hinweis: Diese Circuits sollten auch Messanweisungen für jedes der zu samplenden Qubits enthalten.
  • Eine Sammlung von Parameterwerten θk\theta_k, an die der Circuit gebunden wird (nur erforderlich, wenn Parameter-Objekte verwendet werden, die zur Laufzeit gebunden werden müssen)
  • (Optional) eine Anzahl von Shots, mit denen der Circuit gemessen werden soll

Der folgende Code zeigt ein Beispielset vektorisierter Eingaben für das Sampler-Primitive und führt sie auf einem IBM®-Backend als einzelnes RuntimeJobV2 -Objekt aus.

# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util

_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_ibm_runtime": "qiskit-ibm-runtime"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
from qiskit.circuit import (
Parameter,
QuantumCircuit,
ClassicalRegister,
QuantumRegister,
)
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.quantum_info import SparsePauliOp
from qiskit.primitives.containers import BitArray

from qiskit_ibm_runtime import QiskitRuntimeService

# Legacy server-side primitive before qiskit-ibm-runtime 0.50.0
# from qiskit_ibm_runtime import Sampler

# Client-side primitive introduced in qiskit-ibm-runtime 0.50.0
from qiskit_ibm_runtime.executor_sampler import Sampler

import numpy as np

# Instantiate runtime service and get
# the least busy backend
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)

# Define a circuit with two parameters.
circuit = QuantumCircuit(2)
circuit.h(0)
circuit.cx(0, 1)
circuit.ry(Parameter("a"), 0)
circuit.rz(Parameter("b"), 0)
circuit.cx(0, 1)
circuit.h(0)
circuit.measure_all()

# Transpile the circuit
pm = generate_preset_pass_manager(optimization_level=1, backend=backend)
transpiled_circuit = pm.run(circuit)
layout = transpiled_circuit.layout

# Now define a sweep over parameter values, the last axis of dimension 2 is
# for the two parameters "a" and "b"
params = np.vstack(
[
np.linspace(-np.pi, np.pi, 100),
np.linspace(-4 * np.pi, 4 * np.pi, 100),
]
).T

sampler_pub = (transpiled_circuit, params)

# Instantiate the new Sampler object, then run the transpiled circuit
# using the set of parameters and observables.
sampler = Sampler(mode=backend)
job = sampler.run([sampler_pub])
result = job.result()

Ausgaben​

Nachdem ein oder mehrere PUBs zur Ausführung an eine QPU gesendet wurden und ein Job erfolgreich abgeschlossen ist, werden die Daten als PrimitiveResult-Container-Objekt zurückgegeben, auf das durch den Aufruf der RuntimeJobV2.result()-Methode zugegriffen wird. Das PrimitiveResult enthält eine iterierbare Liste von SamplerPubResult-Objekten, die die Ausführungsergebnisse für jedes PUB enthalten. Diese Daten sind Samples der Circuit-Ausgabe.

Jedes Element dieser Liste entspricht einem PUB, das an die run()-Methode des Primitive übergeben wurde (ein Job, der beispielsweise mit 20 PUBs übermittelt wurde, gibt ein PrimitiveResult-Objekt zurück, das eine Liste mit 20 SamplerPubResult-Objekten enthält – eines für jedes PUB).

Jedes SamplerPubResult-Objekt besitzt sowohl ein data- als auch ein metadata-Attribut.

  • Das data-Attribut ist ein angepasstes DataBin, das die eigentlichen Messwerte, Standardabweichungen und so weiter enthält. Die Data-Bins sind dict-ähnliche Objekte, die ein BitArray pro ClassicalRegister im Circuit enthalten.
  • Die BitArray-Klasse ist ein Container für geordnete Shot-Daten. Sie speichert die gesampleten Bitstrings als Bytes in einem zweidimensionalen Array. Die äußerste linke Achse dieses Arrays läuft über geordnete Shots, während die äußerste rechte Achse über Bytes läuft.
  • Das metadata-Attribut enthält Informationen über die verwendeten Laufzeitoptionen (später im Abschnitt Ergebnis-Metadaten dieser Seite erläutert).

Hier ein schematischer Überblick über die PrimitiveResult-Datenstruktur:

└── PrimitiveResult
├── SamplerPubResult[0]
│ ├── metadata
│ └── data ## In the form of a DataBin object
│ ├── NAME_OF_CLASSICAL_REGISTER
│ │ └── BitArray of count data (default is 'meas')
| |
│ └── NAME_OF_ANOTHER_CLASSICAL_REGISTER
│ └── BitArray of count data (exists only if more than one
| ClassicalRegister was specified in the circuit)
├── SamplerPubResult[1]
| ├── metadata
| └── data ## In the form of a DataBin object
| └── NAME_OF_CLASSICAL_REGISTER
| └── BitArray of count data for second pub
├── ...
├── ...
└── ...

Kurz gesagt gibt ein einzelner Job ein PrimitiveResult-Objekt zurück und enthält eine Liste aus einem oder mehreren SamplerPubResult-Objekten. Diese SamplerPubResult-Objekte speichern dann die Messdaten für jedes PUB, das an den Job übermittelt wurde.

Als erstes Beispiel schauen wir uns den folgenden Zehn-Qubit-Circuit an:

# generate a ten-qubit GHZ circuit
circuit = QuantumCircuit(10)
circuit.h(0)
circuit.cx(range(0, 9), range(1, 10))

# append measurements with the `measure_all` method
circuit.measure_all()

# transpile the circuit
transpiled_circuit = pm.run(circuit)

# run the Sampler job and retrieve the results
sampler = Sampler(mode=backend)
job = sampler.run([transpiled_circuit])
result = job.result()

# the data bin contains one BitArray
data = result[0].data
print(f"Databin: {data}\n")

# to access the BitArray, use the key "meas", which is the default name of
# the classical register when this is added by the `measure_all` method
array = data.meas
print(f"BitArray: {array}\n")
print(f"The shape of register `meas` is {data.meas.array.shape}.\n")
print(f"The bytes in register `alpha`, shot by shot:\n{data.meas.array}\n")
Databin: DataBin(meas=BitArray(<shape=(), num_shots=4096, num_bits=10>))

BitArray: BitArray(<shape=(), num_shots=4096, num_bits=10>)

The shape of register `meas` is (4096, 2).

The bytes in register `alpha`, shot by shot:
[[ 3 247]
[ 0 0]
[ 3 223]
...
[ 0 255]
[ 0 0]
[ 0 0]]

Manchmal ist es praktisch, vom Bytes-Format im BitArray auf Bitstrings umzusteigen. Die get_count-Methode gibt ein Dictionary zurück, das Bitstrings auf die Anzahl ihrer Vorkommen abbildet.

# optionally, convert away from the native BitArray format to a dictionary format
counts = data.meas.get_counts()
print(f"Counts: {counts}")
Counts: {'1111110111': 28, '0000000000': 1680, '1111011111': 35, '1111111111': 1467, '1110000000': 28, '0000000010': 15, '1111111011': 15, '1110111111': 7, '1000000000': 52, '1111101111': 16, '1111110000': 22, '0000000101': 3, '0001111101': 1, '0000000111': 17, '0000000001': 66, '1111111110': 119, '0000001000': 15, '0011111111': 24, '1111111101': 38, '0000001111': 32, '1100000000': 39, '1101111111': 51, '0000011111': 34, '1110000010': 2, '0010000000': 25, '0111111111': 21, '0001111111': 34, '1011111111': 16, '1000011111': 3, '0000001101': 1, '1111100000': 22, '1111111100': 22, '1111111000': 8, '0010001011': 1, '0011000000': 1, '0000000011': 16, '0011111100': 2, '1011110001': 1, '1101111100': 1, '1101111101': 1, '0000000110': 2, '0000000100': 2, '0001111110': 6, '0000001110': 4, '0011110111': 2, '1101111110': 3, '0111111011': 1, '0110000000': 1, '0000001011': 2, '1111110110': 3, '0111111000': 1, '1010000000': 2, '0001110111': 1, '1111101000': 1, '0010011110': 1, '1111100111': 1, '0111110111': 1, '0100000000': 5, '1101110111': 1, '0000001001': 1, '0010000001': 1, '1111000001': 1, '0000010000': 3, '1111101110': 1, '1111000000': 8, '0001011111': 3, '1000000001': 1, '1111010111': 2, '0010011111': 1, '1011111110': 1, '1101101111': 1, '1111011110': 2, '1111111010': 1, '1111010000': 1, '1101000000': 1, '1100001111': 1, '1100011111': 1, '0000111111': 4, '0010111111': 1, '1000000110': 1, '1110111110': 1, '1101011111': 2, '0000011000': 1, '1101110000': 2, '1011111011': 1, '1000111111': 1, '1011000000': 1, '1110000001': 2, '0101111111': 2, '1111110101': 1, '0010000011': 1, '0000011110': 1, '0000011101': 1, '0111000000': 1, '0001110000': 1, '0001000000': 4, '1101111000': 1, '1011111000': 1, '0111110000': 1, '1101110110': 1, '0011011111': 1, '0000100000': 2, '1111101100': 1, '1100001000': 1, '1010000011': 1, '0011111110': 1, '1010001110': 1, '1000001000': 1, '1000000011': 1, '1111101011': 1}

Wenn ein Circuit mehr als ein klassisches Register enthält, werden die Ergebnisse in verschiedenen BitArray-Objekten gespeichert. Das folgende Beispiel ändert den vorherigen Code, indem das klassische Register in zwei separate Register aufgeteilt wird:

# generate a ten-qubit GHZ circuit with two classical registers
circuit = QuantumCircuit(
qreg := QuantumRegister(10),
alpha := ClassicalRegister(1, "alpha"),
beta := ClassicalRegister(9, "beta"),
)
circuit.h(0)
circuit.cx(range(0, 9), range(1, 10))

# append measurements with the `measure_all` method
circuit.measure([0], alpha)
circuit.measure(range(1, 10), beta)

# transpile the circuit
transpiled_circuit = pm.run(circuit)

# run the Sampler job and retrieve the results
sampler = Sampler(mode=backend)
job = sampler.run([transpiled_circuit])
result = job.result()

# the data bin contains two BitArrays, one per register, and can be accessed
# as attributes using the registers' names
data = result[0].data
print(f"BitArray for register 'alpha': {data.alpha}")
print(f"BitArray for register 'beta': {data.beta}")
BitArray for register 'alpha': BitArray(<shape=(), num_shots=4096, num_bits=1>)
BitArray for register 'beta': BitArray(<shape=(), num_shots=4096, num_bits=9>)

BitArray-Objekte für performante Nachverarbeitung verwenden​

Da Arrays im Allgemeinen eine bessere Performance als Dictionaries bieten, ist es ratsam, die Nachverarbeitung direkt an den BitArray-Objekten statt an Count-Dictionaries durchzuführen. Die BitArray-Klasse bietet eine Reihe von Methoden für gängige Nachverarbeitungsoperationen:

print(f"The shape of register `alpha` is {data.alpha.array.shape}.")
print(f"The bytes in register `alpha`, shot by shot:\n{data.alpha.array}\n")

print(f"The shape of register `beta` is {data.beta.array.shape}.")
print(f"The bytes in register `beta`, shot by shot:\n{data.beta.array}\n")

# post-select the bitstrings of `beta` based on having sampled "1" in `alpha`
mask = data.alpha.array == 0b1
ps_beta = data.beta[mask[:, 0]]
print(f"The shape of `beta` after post-selection is {ps_beta.array.shape}.")
print(f"The bytes in `beta` after post-selection:\n{ps_beta.array}")

# get a slice of `beta` to retrieve the first three bits
beta_sl_bits = data.beta.slice_bits([0, 1, 2])
print(
f"The shape of `beta` after bit-wise slicing is {beta_sl_bits.array.shape}."
)
print(f"The bytes in `beta` after bit-wise slicing:\n{beta_sl_bits.array}\n")

# get a slice of `beta` to retrieve the bytes of the first five shots
beta_sl_shots = data.beta.slice_shots([0, 1, 2, 3, 4])
print(
f"The shape of `beta` after shot-wise slicing is {beta_sl_shots.array.shape}."
)
print(
f"The bytes in `beta` after shot-wise slicing:\n{beta_sl_shots.array}\n"
)

# calculate the expectation value of diagonal operators on `beta`
ops = [SparsePauliOp("ZZZZZZZZZ"), SparsePauliOp("IIIIIIIIZ")]
exp_vals = data.beta.expectation_values(ops)
for o, e in zip(ops, exp_vals):
print(f"Exp. val. for observable `{o}` is: {e}")

# concatenate the bitstrings in `alpha` and `beta` to "merge" the results of the two
# registers
merged_results = BitArray.concatenate_bits([data.alpha, data.beta])
print(f"\nThe shape of the merged results is {merged_results.array.shape}.")
print(f"The bytes of the merged results:\n{merged_results.array}\n")
The shape of register `alpha` is (4096, 1).
The bytes in register `alpha`, shot by shot:
[[1]
[0]
[1]
...
[0]
[1]
[0]]

The shape of register `beta` is (4096, 2).
The bytes in register `beta`, shot by shot:
[[ 1 255]
[ 0 0]
[ 1 255]
...
[ 0 0]
[ 1 255]
[ 0 0]]

The shape of `beta` after post-selection is (0, 2).
The bytes in `beta` after post-selection:
[]
The shape of `beta` after bit-wise slicing is (4096, 1).
The bytes in `beta` after bit-wise slicing:
[[7]
[0]
[7]
...
[0]
[7]
[0]]

The shape of `beta` after shot-wise slicing is (5, 2).
The bytes in `beta` after shot-wise slicing:
[[ 1 255]
[ 0 0]
[ 1 255]
[ 1 255]
[ 1 255]]

Exp. val. for observable `SparsePauliOp(['ZZZZZZZZZ'],
coeffs=[1.+0.j])` is: 0.0595703125
Exp. val. for observable `SparsePauliOp(['IIIIIIIIZ'],
coeffs=[1.+0.j])` is: 0.02783203125

The shape of the merged results is (4096, 2).
The bytes of the merged results:
[[ 3 255]
[ 0 0]
[ 3 255]
...
[ 0 0]
[ 3 255]
[ 0 0]]

Ergebnismetadaten​

Zusätzlich zu den Ausführungsergebnissen enthalten sowohl das PrimitiveResult- als auch das SamplerPubResult-Objekt ein Metadata-Attribut zum übermittelten Job. Die Metadaten mit Informationen zu allen übermittelten PUBs (wie den verschiedenen verfügbaren Laufzeitoptionen) findest du in PrimitiveResult.metatada, während die für jedes PUB spezifischen Metadaten in SamplerPubResult.metadata zu finden sind.

Der clientseitige Sampler übermittelt seine Workload als Executor-Job. Die Metadaten des zugrunde liegenden Executor-Ergebnisses, die Informationen zur Ausführungszeit enthalten, sogenannte Execution Spans, sind unter dem Schlüssel "executor" von PrimitiveResult.metadata verfügbar.

hinweis

Im Metadatenfeld können Implementierungen der Primitives beliebige, für sie relevante Ausführungsinformationen zurückgeben, und es gibt keine Schlüssel-Wert-Paare, die durch das Basisprimitive garantiert werden. Die zurückgegebenen Metadaten können daher je nach Implementierung des Primitives unterschiedlich sein.

# Print out the results metadata
print("The metadata of the PrimitiveResult is:")
for key, val in result.metadata.items():
print(f"'{key}' : {val},")

print("\nThe metadata of the PubResult result is:")
for key, val in result[0].metadata.items():
print(f"'{key}' : {val},")
The metadata of the PrimitiveResult is:
'execution' : {'execution_spans': ExecutionSpans([DoubleSliceSpan(<start='2026-09-01 07:46:47', stop='2026-09-01 07:46:49', size=4096>)])},
'version' : 2,

The metadata of the PubResult result is:
'circuit_metadata' : {},

Execution Spans anzeigen​

Der clientseitige Sampler nutzt Executor, um Jobs auszuführen. Executor führt seine Arbeit in Chunks auf der QPU aus. Die Zeitangaben jedes Chunks stehen in result.metadata["executor"].chunk_timing, einer Liste von ChunkSpan-Objekten. Jedes ChunkSpan hat die Zeitstempel start und stop sowie eine Liste von parts, wobei jeder Part einen Item-Index (idx_item) und die Anzahl der Konfigurationen dieses Items (size) angibt, die im Chunk ausgeführt wurden.

Sampler erzeugt pro PUB ein Executor-Programm-Item, daher ist idx_item der Index des PUB in der Reihenfolge, in der die PUBs an run() übergeben wurden. Wenn Twirling aktiviert ist, zählt jede Randomisierung jedes Parameterwertsatzes als separate Konfiguration.

Um die Zeitangaben zu visualisieren, übergib die Liste an draw_chunk_timings, wie im folgenden Beispiel gezeigt.

Was die Chunk-Zeitstempel bedeuten, wie du das Diagramm liest und warum zwischen Chunks Lücken auftreten können, erfährst du unter Execution Spans anzeigen im Leitfaden zu Executor-Ein- und Ausgaben.

# Define a circuit with one parameter.
circuit = QuantumCircuit(2)
circuit.h(0)
circuit.cx(0, 1)
circuit.ry(Parameter("a"), 0)
circuit.cx(0, 1)
circuit.h(0)
circuit.measure_all()

pm = generate_preset_pass_manager(optimization_level=1, backend=backend)
transpiled_circuit = pm.run(circuit)

# Define two PUBs, each with a different set of parameter values
sampler_pubs = [
(transpiled_circuit, np.random.uniform(size=(10, 1))),
(transpiled_circuit, np.random.uniform(size=(20, 1))),
]

job = sampler.run(sampler_pubs, shots=1024)
result = job.result()
from qiskit_ibm_runtime.visualization import draw_chunk_timings

# Note the "executor" key: the timing comes from the underlying Executor job
chunk_timing = result.metadata["executor"].chunk_timing

for idx, span in enumerate(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" PUB {part.idx_item}: {part.size} configurations")

fig = draw_chunk_timings(chunk_timing, names="Sampler job")

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

# Save to a file
# fig.write_html("chunk_timings.html")
Chunk 0: 2026-11-06 01:36:36.203348+00:00 to 2026-11-06 01:36:45.420844+00:00 (9.217 s)
PUB 0: 10 configurations
PUB 1: 20 configurations

Execution Spans im Legacy-Sampler​

Ab qiskit-ibm-runtime v0.50.0 ist der serverseitige Legacy-Sampler veraltet. Der clientseitige Sampler gibt keine ExecutionSpans-Objekte zurück; verwende stattdessen die zuvor beschriebenen Chunk-Zeitangaben. Anweisungen zur Migration findest du unter Zu clientseitigen Primitives migrieren.

Klicke, um mehr zu erfahren.

Die Ergebnisse von Jobs des serverseitigen Legacy-Sampler enthalten Informationen zur Ausführungszeit in result.metadata["execution"]["execution_spans"], und zwar in Form eines ExecutionSpans-Objekts. Dieses Objekt ist ein listenähnlicher Container aus ExecutionSpan-Objekten, von denen jedes eine Startzeit, eine Stoppzeit und eine Angabe darüber enthält, welche Shots im jeweiligen Span gesammelt wurden.

Ein Execution Span gibt an, welche Daten während seines Zeitfensters ausgeführt wurden, indem er eine ExecutionSpan.mask-Methode bereitstellt. Diese Methode gibt für einen beliebigen PUB-Index eine boolesche Maske zurück, die für alle im Zeitfenster ausgeführten Shots True ist. Hat ein PUB beispielsweise die Form (2, 3) und wurde mit vier Shots ausgeführt, hat die Maske die Form (2, 3, 4). Weitere Details findest du auf der execution_span-API-Seite.

from qiskit.primitives import BitArray

spans = result.metadata["execution"]["execution_spans"]

# Hole die Maske des 1. PUB für den 0. Span.
mask = spans[0].mask(0)

# Erstelle ein neues Bit-Array, das nur die in diesem Span gesammelten PUB-0-Daten enthält.
bits = result[0].data.meas
filtered_data = BitArray(bits.array[mask], bits.num_bits)

# Nimm die Teilmenge der Spans, die sich auf Daten in den PUBs 0 oder 2 beziehen
spans.filter_by_pub([0, 2])

# Zeige globale Informationen zur Sammlung der Execution Spans an
print("Number of execution spans:", len(spans))
print(" Start of the first span:", spans.start)
print(" End of the last span:", spans.stop)
print(" Total duration (s):", spans.duration)

Um Legacy-Execution-Spans zu visualisieren, verwende die Funktion draw_execution_spans.

Es ist möglich, dass sich die Zeitfenster verschiedener Execution Spans überschneiden. Das liegt nicht daran, dass eine QPU mehrere Ausführungen gleichzeitig durchgeführt hat, sondern ist ein Artefakt bestimmter klassischer Verarbeitungsschritte, die möglicherweise parallel zur Quantenausführung stattfinden. Die Garantie besteht darin, dass die referenzierten Daten definitiv im angegebenen Execution Span stattgefunden haben, aber nicht unbedingt, dass die Grenzen des Zeitfensters so eng wie möglich gesetzt sind.