Zum Hauptinhalt springen

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.4.0
qiskit-ibm-runtime~=0.46.1

Diese Seite gibt einen Überblick über die Eingaben und Ausgaben des Qiskit Runtime Sampler-Primitive, das Workloads auf IBM Quantum®-Rechenressourcen 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 aus dem Primitive angegebenen Laufzeitoptionen abhängt.

Eingaben

Jedes PUB hat das folgende Format:

(<einzelner Circuit>, <ein oder mehrere optionale Parameterwerte>, <optionale 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 jeden der zu messenden Qubits enthalten.
  • Eine Sammlung von Parameterwerten, gegen die der Circuit gebunden wird θk\theta_k (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 demonstriert eine Beispielmenge vektorisierter Eingaben für das Sampler-Primitive und führt sie auf einem IBM®-Backend als einzelnes RuntimeJobV2-Objekt aus.

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-ibm-runtime
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,
SamplerV2 as 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).

Das Folgende ist ein visueller Ü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:
[[ 0 0]
[ 3 255]
[ 0 0]
...
[ 3 255]
[ 2 255]
[ 3 255]]

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: {'0000000000': 1649, '1111111111': 1344, '1111111000': 26, '1101111111': 40, '1111110000': 20, '0010000000': 32, '1000000000': 67, '1111110110': 4, '0000011110': 4, '0000000001': 78, '0010100000': 1, '1100000000': 37, '1111111110': 126, '1111110111': 35, '1111011111': 32, '0011111000': 1, '1011110111': 1, '0000011111': 48, '1111000000': 14, '0110000000': 1, '1110111110': 2, '1110011111': 4, '1111100000': 19, '1101111000': 1, '1111111011': 8, '0001011111': 3, '1110000000': 31, '0000000111': 25, '1110000001': 3, '0011111111': 24, '0000100000': 7, '1111111101': 30, '1111101111': 16, '0111111111': 37, '0000011101': 4, '0101111111': 4, '1011111110': 2, '0000000010': 17, '1011111111': 20, '0000100111': 1, '0010000111': 1, '1011010000': 1, '1101101111': 2, '1011110000': 1, '1000000001': 4, '0000001000': 23, '0011111110': 8, '1111111001': 1, '1100111111': 2, '0000011000': 2, '0001111110': 2, '0000111111': 20, '0001111111': 33, '1110111111': 11, '1010000000': 3, '0111011111': 2, '0000000100': 2, '0000000110': 2, '0000001111': 22, '0111101111': 1, '0000010111': 1, '0000000011': 15, '0001000010': 1, '1111111100': 19, '1111101000': 1, '0000001110': 2, '1011110100': 1, '0001000000': 11, '1001111111': 2, '0100000000': 6, '1100000011': 2, '1000001110': 1, '1100001111': 1, '0000010000': 3, '1101111110': 5, '0001111101': 1, '0001110111': 1, '0011000000': 2, '0111101110': 1, '1100000001': 1, '1111000001': 1, '0000000101': 1, '1101110111': 2, '0011111011': 1, '0000111110': 1, '1111101110': 3, '1111001000': 1, '1011111100': 1, '1111110101': 2, '1101001111': 1, '1111011110': 3, '1000011111': 1, '0000001001': 2, '1111010000': 1, '1110100010': 1, '1111110001': 2, '1101110000': 2, '0000010100': 1, '0111111110': 2, '0001000001': 1, '1000010000': 1, '1111011100': 1, '0111111100': 1, '1011101111': 1, '0000111101': 1, '1100011111': 2, '1101100000': 1, '1111011011': 1, '0010011111': 1, '0000110111': 3, '1111100010': 1, '1110111101': 1, '0000111001': 1, '1111100001': 1, '0001111100': 1, '1110011110': 1, '1100000010': 1, '0011110000': 1, '0001100111': 1, '1111010111': 1, '0010000001': 1, '0010000011': 1, '1101000111': 1, '1011111101': 1, '0000001100': 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:
[[0]
[0]
[0]
...
[0]
[0]
[0]]

The shape of register `beta` is (4096, 2).
The bytes in register `beta`, shot by shot:
[[ 0 0]
[ 1 248]
[ 0 0]
...
[ 0 0]
[ 0 0]
[ 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:
[[0]
[0]
[0]
...
[0]
[0]
[0]]

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

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

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

Ergebnismetadaten

Neben den Ausführungsergebnissen enthalten sowohl das PrimitiveResult- als auch das SamplerPubResult-Objekt ein Metadaten-Attribut über den eingereichten Job. Die Metadaten mit Informationen für alle eingereichten PUBs (wie die verschiedenen verfügbaren Laufzeitoptionen) findest du in PrimitiveResult.metadata, während die PUB-spezifischen Metadaten in SamplerPubResult.metadata zu finden sind.

Die Sampler-Ergebnismetadaten enthalten außerdem Informationen zur Ausführungszeit, die als Execution Span bezeichnet werden.

hinweis

Im Metadatenfeld können primitive Implementierungen 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 primitiver Implementierung 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-05-13 14:23:00', stop='2026-05-13 14:23:02', size=4096>)])},
'version' : 2,

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

Execution Spans anzeigen

Die Ergebnisse von SamplerV2-Jobs, die in Qiskit Runtime ausgeführt werden, enthalten in ihren Metadaten Informationen zur Ausführungszeit. Diese Zeitinformationen können genutzt werden, um obere und untere Zeitstempelgrenzen dafür festzulegen, wann bestimmte Shots auf dem QPU ausgeführt wurden. Shots werden in ExecutionSpan-Objekte gruppiert, die jeweils eine Startzeit, eine Stoppzeit und eine Angabe darüber enthalten, 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 Primitive Unified Block (PUB)-Index eine boolesche Maske zurück, die für alle im Zeitfenster ausgeführten Shots True ist. PUBs werden nach der Reihenfolge indiziert, in der sie dem Sampler-Run-Aufruf übergeben wurden. 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.

Um Execution-Span-Informationen anzuzeigen, überprüfe die Metadaten des von SamplerV2 zurückgegebenen Ergebnisses, das in Form eines ExecutionSpans-Objekts vorliegt. Dieses Objekt ist ein listenähnlicher Container, der Instanzen von Unterklassen von ExecutionSpan enthält, wie z. B. SliceSpan.

Beispiel:

# Define two circuits, each with one parameter with two parameters.
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)

params = np.random.uniform(size=(2, 3)).T

sampler_pub = (transpiled_circuit, params)

# Instantiate the new Estimator object, then run the transpiled circuit
# using the set of parameters and observables.

job = sampler.run([sampler_pub], shots=4)

result = job.result()
spans = job.result().metadata["execution"]["execution_spans"]
print(spans)
ExecutionSpans([DoubleSliceSpan(<start='2026-05-13 14:23:20', stop='2026-05-13 14:23:21', size=24>)])
from qiskit.primitives import BitArray

# Get the mask of the 1st PUB for the 0th span.
mask = spans[0].mask(0)

# Decide whether the 0th shot of parameter set (1, 2) occurred in this span.
in_this_span = mask[2, 1, 0]

# Create a new bit array containing only the PUB-1 data collected during this span.
bits = result[0].data.meas
filtered_data = BitArray(bits.array[mask], bits.num_bits)

Execution Spans können gefiltert werden, um Informationen zu bestimmten PUBs zu erhalten, die nach ihren Indizes ausgewählt werden:

# take the subset of spans that reference data in PUBs 0 or 2
spans.filter_by_pub([0, 2])
ExecutionSpans([DoubleSliceSpan(<start='2026-05-13 14:23:20', stop='2026-05-13 14:23:21', size=24>)])

Globale Informationen zur Gesamtheit der Execution Spans anzeigen:

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)
Number of execution spans: 1
Start of the first span: 2026-05-13 14:23:20.441518
End of the last span: 2026-05-13 14:23:21.564845
Total duration (s): 1.123327

Einen bestimmten Span extrahieren und untersuchen:

spans.sort()
print(" Start of first span:", spans[0].start)
print(" End of first span:", spans[0].stop)
print("#shots in first span:", spans[0].size)
Start of first span: 2026-05-13 14:23:20.441518
End of first span: 2026-05-13 14:23:21.564845
#shots in first span: 24
hinweis

Es ist möglich, dass sich die Zeitfenster verschiedener Execution Spans überschneiden. Das liegt nicht daran, dass ein 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.