Wejścia i wyjścia Samplera
Wersje pakietów
Kod na tej stronie został opracowany z użyciem poniższych wymagań. Zalecamy używanie tych wersji lub nowszych.
qiskit[all]~=2.4.0
qiskit-ibm-runtime~=0.46.1
Ta strona daje przegląd wejść i wyjść prymitywu Sampler Qiskit Runtime, który wykonuje zadania na zasobach obliczeniowych IBM Quantum®. Sampler pozwala na efektywne definiowanie zwektoryzowanych zadań za pomocą struktury danych zwanej Primitive Unified Bloc (PUB). Są one używane jako dane wejściowe do metody run() dla prymitywu Sampler, który wykonuje zdefiniowane zadanie jako zadanie. Następnie, po zakończeniu zadania, wyniki są zwracane w formacie zależnym zarówno od użytych PUBów, jak i opcji runtime określonych przez prymityw.
Wejścia
Każdy PUB ma format:
(<pojedynczy obwód>, <jedna lub więcej opcjonalnych wartości parametrów>, <opcjonalne shots>),
Może być wiele elementów wartości parametrów, a każdy element może być tablicą lub pojedynczym parametrem, w zależności od wybranego obwodu. Ponadto wejście musi zawierać pomiary.
Dla prymitywu Sampler PUB może zawierać co najwyżej trzy wartości:
- Pojedynczy
QuantumCircuit, który może zawierać jeden lub więcej obiektówParameterUwaga: Te obwody powinny również zawierać instrukcje pomiarowe dla każdego z qubitów do próbkowania. - Kolekcja wartości parametrów do powiązania obwodu z (wymagana tylko wtedy, gdy używane są obiekty
Parameter, które muszą być powiązane w czasie wykonania) - (Opcjonalnie) liczba shotów do pomiaru obwodu
Poniższy kod demonstruje przykładowy zestaw zwektoryzowanych wejść do prymitywu Sampler i wykonuje je na backendzie IBM® jako pojedynczy obiekt RuntimeJobV2.
# 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()
Wyjścia
Po wysłaniu jednego lub więcej PUBów do QPU do wykonania i pomyślnym zakończeniu zadania dane są zwracane jako obiekt kontenera PrimitiveResult dostępny poprzez wywołanie metody RuntimeJobV2.result(). PrimitiveResult zawiera iterowalną listę obiektów SamplerPubResult zawierających wyniki wykonania dla każdego PUBa. Te dane to próbki wyjścia obwodu.
Każdy element tej listy odpowiada PUBowi przesłanemu do metody run() prymitywu (na przykład zadanie przesłane z 20 PUBami zwróci obiekt PrimitiveResult zawierający listę 20 obiektów SamplerPubResult, jeden odpowiadający każdemu PUBowi).
Każdy obiekt SamplerPubResult posiada zarówno atrybut data, jak i metadata.
- Atrybut
datajest dostosowanymDataBinzawierającym rzeczywiste wartości pomiarów, odchylenia standardowe i tym podobne. Kontenery danych to obiekty słownikopodobne zawierające jedenBitArraynaClassicalRegisterw obwodzie. - Klasa
BitArrayto kontener dla uporządkowanych danych shotów. Przechowuje próbkowane ciągi bitów jako bajty wewnątrz dwuwymiarowej tablicy. Najbardziej lewa oś tej tablicy przebiega po uporządkowanych shotach, a najbardziej prawa — po bajtach. - Atrybut
metadatazawiera informacje o użytych opcjach runtime (wyjaśnione później w sekcji Metadane wyników tej strony).
Poniżej przedstawiono wizualny zarys struktury danych PrimitiveResult:
└── 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
├── ...
├── ...
└── ...
Krótko mówiąc, jedno zadanie zwraca obiekt PrimitiveResult i zawiera listę jednego lub więcej obiektów SamplerPubResult. Te obiekty SamplerPubResult przechowują następnie dane pomiarowe dla każdego PUBa przesłanego do zadania.
Jako pierwszy przykład przyjrzyjmy się poniższemu obwodowi dziesięcioqubitowemu:
# 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]]
Czasami wygodnie jest przekonwertować z formatu bajtowego w BitArray na ciągi bitów. Metoda get_count zwraca słownik mapujący ciągi bitów na liczbę ich wystąpień.
# 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}
Gdy obwód zawiera więcej niż jeden rejestr klasyczny, wyniki są przechowywane w różnych obiektach BitArray. Poniższy przykład modyfikuje poprzedni fragment, dzieląc rejestr klasyczny na dwa odrębne rejestry:
# 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>)
Używanie obiektów BitArray do wydajnego post-przetwarzania
Ponieważ tablice zazwyczaj oferują lepszą wydajność w porównaniu ze słownikami, zaleca się wykonywanie wszelkiego post-przetwarzania bezpośrednio na obiektach BitArray, a nie na słownikach zliczeń. Klasa BitArray oferuje szereg metod do wykonywania pewnych typowych operacji post-przetwarzania:
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]]
Metadane wyników
Oprócz wyników wykonania, zarówno obiekty PrimitiveResult, jak i SamplerPubResult zawierają atrybut metadanych dotyczący przesłanego zadania. Metadane zawierające informacje dla wszystkich przesłanych PUBów (takie jak różne opcje runtime dostępne) można znaleźć w PrimitiveResult.metatada, natomiast metadane specyficzne dla każdego PUBa znajdują się w SamplerPubResult.metadata.
Metadane wyników Samplera zawierają również informacje o czasie wykonania zwane zakresem wykonania.
W polu metadanych implementacje prymitywów mogą zwracać wszelkie informacje dotyczące wykonania, które są dla nich istotne, i nie ma gwarantowanych par klucz-wartość przez bazowy prymityw. Zatem zwrócone metadane mogą różnić się w różnych implementacjach prymitywów.
# 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' : {},
Przeglądanie zakresów wykonania
Wyniki zadań SamplerV2 wykonanych w Qiskit Runtime zawierają informacje o czasie wykonania w swoich metadanych.
Te informacje o czasie mogą być używane do wyznaczenia górnych i dolnych granic czasowych, kiedy określone shoty były wykonywane na QPU.
Shoty są grupowane w obiekty ExecutionSpan, z których każdy wskazuje czas rozpoczęcia, czas zakończenia i specyfikację, które shoty zostały zebrane w danym zakresie.
Zakres wykonania określa, które dane były wykonywane podczas jego okna, udostępniając metodę ExecutionSpan.mask. Ta metoda, dla dowolnego indeksu Primitive Unified Block (PUB), zwraca maskę logiczną, która jest True dla wszystkich shotów wykonanych podczas jej okna. PUBy są indeksowane według kolejności, w jakiej zostały przekazane do wywołania Sampler run. Jeśli na przykład PUB ma kształt (2, 3) i był uruchamiany z czterema shotami, kształt maski wynosi (2, 3, 4). Pełne szczegóły znajdziesz na stronie API execution_span.
Aby wyświetlić informacje o zakresie wykonania, przejrzyj metadane wyników zwróconych przez SamplerV2, które mają postać obiektu ExecutionSpans. Ten obiekt to kontener listopodobny zawierający instancje podklas ExecutionSpan, takich jak SliceSpan.
Przykład:
# 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)
Zakresy wykonania można filtrować, aby zawierały informacje dotyczące określonych PUBów, wybranych według ich indeksów:
# 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>)])
Wyświetl globalne informacje o kolekcji zakresów wykonania:
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
Wyodrębnij i sprawdź konkretny zakres:
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
Możliwe jest, że okna czasowe określone przez odrębne zakresy wykonania nakładają się. Nie wynika to z tego, że QPU wykonywał wiele wykonań jednocześnie, ale jest artefaktem pewnego klasycznego przetwarzania, które może odbywać się równolegle z wykonaniem kwantowym. Gwarancją jest to, że przywołane dane na pewno wystąpiły w raportowanym zakresie wykonania, ale niekoniecznie że granice okna czasowego są jak najbardziej ścisłe.