Sampler 입력 및 출력
패키지 버전
이 페이지의 코드는 다음 요구 사항을 사용하여 개발되었습니다. 이 버전 이상을 사용하는 것을 권장합니다.
qiskit[all]~=2.5.2
qiskit-ibm-runtime~=0.47.0
이 페이지는 IBM Quantum® Compute Service에서 워크로드를 실행하는 qiskit-ibm-runtime Sampler 프리미티브의 입력 및 출력에 대한 개요를 제공합니다. Sampler에서는 Primitive Unified Bloc (PUB)라는 데이터 구조를 사용해 벡터화된 워크로드를 효율적으로 정의할 수 있습니다. 이들은 Sampler 프리미티브의 run() 메서드에 대한 입력으로 사용되며, 정의된 워크로드를 작업으로 실행합니다. 그런 다음 작업이 완료된 후 결과는 사용된 PUB와 프리미티브에서 지정된 런타임 옵션에 따라 달라지는 형식으로 반환됩니다.
입력
각 PUB의 형식은 다음과 같습니다:
(<single circuit>, <one or more optional parameter value>, <optional shots>),
여러 개의 parameter values 항목이 있을 수 있으며, 각 항목은 선택한 circuit에 따라 배열 또는 단일 파라미터일 수 있습니다. 또한 입력에는 측정이 포함되어야 합니다.
Sampler 프리미티브의 경우 PUB에는 최대 세 가지 값이 포함될 수 있습니다:
- 하나 이상의
Parameter객체를 포함할 수 있는 단일QuantumCircuit참고: 이러한 circuit에는 샘플링할 각 qubit에 대한 측정 명령도 포함되어야 합니다. - circuit에 대해 바인딩할 파라미터 값 컬렉션 (런타임에 바인딩해야 하는
Parameter객체가 사용된 경우에만 필요) - (선택적으로) circuit을 측정할 shots 수
다음 코드는 Sampler Primitive에 대한 벡터화된 입력 예시 세트를 보여주고, 이를 단일 RuntimeJobV2 객체로서 IBM® backend에서 실행합니다.
# 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()
출력
하나 이상의 PUB가 QPU에 실행을 위해 전송되고 작업이 성공적으로 완료되면, 데이터는 RuntimeJobV2.result() 메서드를 호출하여 접근하는 PrimitiveResult 컨테이너 객체로 반환됩니다. PrimitiveResult에는 각 PUB의 실행 결과를 포함하는 SamplerPubResult 객체의 반복 가능한 목록이 포함됩니다. 이 데이터는 circuit 출력의 샘플입니다.
이 목록의 각 요소는 프리미티브의 run() 메서드에 제출된 PUB에 해당합니다 (예: 20개의 PUB로 제출된 작업은 각 PUB에 해당하는 20개의 SamplerPubResult 객체 목록을 포함하는 PrimitiveResult 객체를 반환합니다).
각 SamplerPubResult 객체는 data 속성과 metadata 속성을 모두 가지고 있습니다.
data속성은 실제 측정 값, 표준 편차 등을 포함하는 사용자 정의된DataBin입니다. 데이터 빈은 circuit의 각ClassicalRegister에 대해 하나의BitArray를 포함하는 딕셔너리 형태의 객체입니다.BitArray클래스는 순서가 있는 shot 데이터의 컨테이너입니다. 샘플링된 비트 문자열을 2차원 배열 내부의 바이트로 저장합니다. 이 배열의 가장 왼쪽 축은 순서가 있는 shots을 따라 실행되고, 가장 오른쪽 축은 바이트를 따라 실행됩니다.metadata속성에는 사용된 런타임 옵션에 관한 정보가 포함됩니다 (이 페이지의 결과 메타데이터 섹션에서 설명됨).
다음은 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
├── ...
├── ...
└── ...
간단히 말하면, 단일 작업은 PrimitiveResult 객체를 반환하고 하나 이상의 SamplerPubResult 객체 목록을 포함합니다. 이러한 SamplerPubResult 객체는 작업에 제출된 각 PUB의 측정 데이터를 저장합니다.
첫 번째 예시로, 다음 10-qubit circuit을 살펴보겠습니다:
# 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]]
BitArray의 바이트 형식에서 비트 문자열로 변환하는 것이 편리할 때가 있습니다. get_count 메서드는 비트 문자열을 발생 횟수에 매핑하는 딕셔너리를 반환합니다.
# 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}
circuit에 두 개 이상의 고전 레지스터가 포함된 경우 결과는 다른 BitArray 객체에 저장됩니다. 다음 예시는 이전 스니펫을 수정하여 고전 레지스터를 두 개의 별도 레지스터로 분할합니다:
# 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 객체 사용
배열이 일반적으로 딕셔너리보다 성능이 우수하기 때문에, 카운트 딕셔너리가 아닌 BitArray 객체에서 직접 후처리를 수행하는 것이 좋습니다. BitArray 클래스는 일반적인 후처리 작업을 수행하는 다양한 메서드를 제공합니다:
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]]
결과 메타데이터
실행 결과 외에도, PrimitiveResult와 SamplerPubResult 객체 모두 제출된 작업에 대한 메타데이터 속성을 포함합니다. 제출된 모든 PUB에 대한 정보(사용 가능한 다양한 runtime 옵션 등)를 포함하는 메타데이터는 PrimitiveResult.metatada에서 찾을 수 있으며, 각 PUB에 특정한 메타데이터는 SamplerPubResult.metadata에서 찾을 수 있습니다.
클라이언트 측 Sampler는 워크로드를 Executor 작업으로 제출합니다. 실행 스팬이라는 실행 타이밍 정보를 포함하는 기반 Executor 결과의 메타데이터는 PrimitiveResult.metadata의 "executor" 키 아래에서 사용할 수 있습니다.
메타데이터 필드에서 프리미티브 구현은 실행과 관련된 모든 정보를 반환할 수 있으며, 기본 프리미티브에 의해 보장되는 키-값 쌍은 없습니다. 따라서 반환된 메타데이터는 다른 프리미티브 구현에서 다를 수 있습니다.
# 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' : {},
실행 스팬 보기
클라이언트 측 Sampler는 Executor를 사용하여 작업을 실행하며, Executor는 QPU에서 작업을 청크 단위로 실행합니다. 각 청크의 타이밍은 ChunkSpan 객체의 리스트인 result.metadata["executor"].chunk_timing에 있습니다. 각 ChunkSpan에는 start 및 stop 타임스탬프와 parts 리스트가 있으며, 각 part는 항목 인덱스(idx_item)와 해당 청크에서 실행된 그 항목의 구성 수(size)를 나타냅니다.
Sampler는 PUB마다 Executor 프로그램 항목을 하나씩 만들므로, idx_item은 PUB가 run()에 주어진 순서에 따른 PUB의 인덱스입니다. 트월링이 활성화된 경우 각 파라미터 값 집합의 무작위화 하나하나가 별도의 구성으로 집계됩니다.
타이밍을 시각화하려면 다음 예제와 같이 리스트를 draw_chunk_timings에 전달하세요.
청크 타임스탬프의 의미, 플롯을 읽는 방법, 청크 사이에 간격이 나타날 수 있는 이유를 알아보려면 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
레거시 Sampler의 실행 스팬
qiskit-ibm-runtime v0.50.0부터 레거시 서버 측 Sampler는 더 이상 사용되지 않습니다(deprecated). 클라이언트 측 Sampler는 ExecutionSpans 객체를 반환하지 않으므로, 앞에서 설명한 청크 타이밍을 대신 사용하세요. 마이그레이션 방법은 클라이언트 측 primitive로 마이그레이션을 참조하세요.
자세히 보려면 클릭하세요.
레거시 서버 측 Sampler 작업의 결과에는 result.metadata["execution"]["execution_spans"]에 ExecutionSpans 객체 형태로 실행 타이밍 정보가 포함됩니다. 이 객체는 ExecutionSpan 객체의 리스트형 컨테이너이며, 각 객체는 시작 시간, 종료 시간, 그리고 해당 스팬에서 어떤 shots가 수집되었는지에 대한 명세를 나타냅니다.
실행 스팬은 ExecutionSpan.mask 메서드를 제공하여 해당 윈도우 동안 어떤 데이터가 실행되었는지를 지정합니다. 이 메서드는 임의의 PUB 인덱스가 주어지면, 해당 윈도우 동안 실행된 모든 shots에 대해 True인 불리언 마스크를 반환합니다. 예를 들어, PUB의 형태가 (2, 3)이고 4 shots으로 실행된 경우, 마스크의 형태는 (2, 3, 4)입니다. 전체 세부 정보는 execution_span API 페이지를 참조하세요.
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)
레거시 실행 구간(execution span)을 시각화하려면 draw_execution_spans 함수를 사용하세요.
서로 다른 실행 스팬으로 지정된 타임 윈도우가 겹칠 수 있습니다. 이는 QPU가 여러 실행을 동시에 수행하기 때문이 아니라, 양자 실행과 동시에 발생할 수 있는 특정 고전 처리의 부산물입니다. 보장되는 것은 참조된 데이터가 보고된 실행 스팬 내에서 확실히 발생했다는 것이지만, 타임 윈도우의 경계가 가능한 한 촘촘하다는 것은 보장되지 않습니다.