주 콘텐츠로 건너뛰기
이 프로젝트는 베타 단계입니다. 버그를 발견하셨나요? GitHub 이슈 만들기. 질문이나 아이디어가 있으신가요? Discussions에서 질문하기 — 피드백을 기다립니다!
이 페이지의 번역 품질은 어떻습니까?

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에 대해 바인딩할 파라미터 값 컬렉션 θk\theta_k (런타임에 바인딩해야 하는 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가 여러 실행을 동시에 수행하기 때문이 아니라, 양자 실행과 동시에 발생할 수 있는 특정 고전 처리의 부산물입니다. 보장되는 것은 참조된 데이터가 보고된 실행 스팬 내에서 확실히 발생했다는 것이지만, 타임 윈도우의 경계가 가능한 한 촘촘하다는 것은 보장되지 않습니다.