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.5.2
qiskit-ibm-runtime~=0.47.0
Na tej stronie omówiono wejścia i wyjścia prymitywu Sampler qiskit-ibm-runtime, który wykonuje obciążenia na IBM Quantum® Compute Service. Sampler pozwala na efektywne definiowanie zwektoryzowanych obciążeń 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 uruchamia zdefiniowane obciążenie jako zadanie (job). 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:
(<single circuit>, <one or more optional parameter value>, <optional shots>),
Może istnieć wiele elementów parameter values, a każdy z nich może być tablicą lub pojedynczym parametrem, w zależności od wybranego obwodu. Dodatkowo dane wejściowe muszą 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 przedstawia przykładowy zestaw zwektoryzowanych danych wejściowych do prymitywu Sampler i wykonuje je na backendzie IBM® jako pojedynczy obiekt RuntimeJobV2 .
# 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()
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:
[[ 3 247]
[ 0 0]
[ 3 223]
...
[ 0 255]
[ 0 0]
[ 0 0]]
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: {'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}
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:
[[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]]
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.
Sampler po stronie klienta przesyła swoje obciążenie jako zadanie Executor. Metadane wyniku bazowego Executor, które zawierają informacje o czasie wykonania zwane zakresami wykonania, są dostępne pod kluczem "executor" w PrimitiveResult.metadata.
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-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' : {},
Przeglądanie zakresów wykonania
Sampler po stronie klienta używa Executor do uruchamiania zadań, który wykonuje swoją pracę na QPU we fragmentach (chunks). Czas każdego fragmentu znajduje się w result.metadata["executor"].chunk_timing, czyli na liście obiektów ChunkSpan. Każdy ChunkSpan ma znaczniki czasu start i stop oraz listę parts, gdzie każda część podaje indeks elementu (idx_item) i liczbę konfiguracji z tego elementu (size), które zostały uruchomione w danym fragmencie.
Sampler tworzy jeden element programu Executor na każdy PUB, więc idx_item jest indeksem PUB-a, w kolejności, w jakiej PUB-y zostały przekazane do run(). Jeśli włączony jest twirling, każda randomizacja każdego zestawu wartości parametrów liczy się jako osobna konfiguracja.
Aby zwizualizować czasy, przekaż listę do draw_chunk_timings, jak pokazano w poniższym przykładzie.
Aby dowiedzieć się, co oznaczają znaczniki czasu fragmentów, jak czytać wykres i dlaczego między fragmentami mogą pojawiać się przerwy, zobacz Wyświetlanie zakresów wykonania w przewodniku Dane wejściowe i wyjściowe Executor.
# 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
Zakresy wykonania w starszym Sampler
Od wersji qiskit-ibm-runtime v0.50.0 starszy Sampler po stronie serwera jest przestarzały. Sampler po stronie klienta nie zwraca obiektów ExecutionSpans; zamiast tego użyj opisanego wcześniej czasu fragmentów. Instrukcje migracji znajdziesz w Migracja do prymitywów po stronie klienta.
Kliknij, aby dowiedzieć się więcej.
Wyniki zadań ze starszego Sampler po stronie serwera zawierają informacje o czasie wykonania w result.metadata["execution"]["execution_spans"], w postaci obiektu ExecutionSpans. Ten obiekt jest przypominającym listę kontenerem obiektów ExecutionSpan, z których każdy wskazuje czas rozpoczęcia, czas zakończenia oraz określenie, 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 PUB, zwraca maskę logiczną, która jest True dla wszystkich shotów wykonanych podczas jej okna. 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.
from qiskit.primitives import BitArray
spans = result.metadata["execution"]["execution_spans"]
# Get the mask of the 1st PUB for the 0th span.
mask = spans[0].mask(0)
# Create a new bit array containing only the PUB-0 data collected during this span.
bits = result[0].data.meas
filtered_data = BitArray(bits.array[mask], bits.num_bits)
# Take the subset of spans that reference data in PUBs 0 or 2
spans.filter_by_pub([0, 2])
# View global information about the collection of execution spans
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)
Aby zwizualizować starsze zakresy wykonania, użyj funkcji draw_execution_spans.
Okna czasowe określone przez odrębne zakresy wykonania mogą się nakładać. Nie wynika to z tego, że QPU wykonywał wiele wykonań jednocześnie, lecz jest efektem 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.