주 콘텐츠로 건너뛰기

Sampler 입력 및 출력

패키지 버전

이 페이지의 코드는 다음 요구 사항을 사용하여 개발되었습니다. 이 버전 이상을 사용하는 것을 권장합니다.

qiskit[all]~=2.4.0
qiskit-ibm-runtime~=0.46.1

이 페이지는 IBM Quantum® 컴퓨팅 리소스에서 워크로드를 실행하는 Qiskit Runtime Sampler 프리미티브의 입력 및 출력에 대한 개요를 제공합니다. Sampler는 Primitive Unified Bloc (PUB)로 알려진 데이터 구조를 사용하여 벡터화된 워크로드를 효율적으로 정의할 수 있습니다. 이들은 Sampler 프리미티브의 run() 메서드에 대한 입력으로 사용되며, 정의된 워크로드를 작업으로 실행합니다. 그런 다음 작업이 완료된 후 결과는 사용된 PUB와 프리미티브에서 지정된 런타임 옵션에 따라 달라지는 형식으로 반환됩니다.

입력

각 PUB의 형식은 다음과 같습니다:

(<단일 circuit>, <하나 이상의 선택적 파라미터 값>, <선택적 shots>),

여러 개의 parameter values 항목이 있을 수 있으며, 각 항목은 선택한 circuit에 따라 배열 또는 단일 파라미터일 수 있습니다. 또한 입력에는 측정이 포함되어야 합니다.

Sampler 프리미티브의 경우 PUB에는 최대 세 가지 값이 포함될 수 있습니다:

  • 하나 이상의 Parameter 객체를 포함할 수 있는 단일 QuantumCircuit 참고: 이러한 circuit에는 샘플링할 각 qubit에 대한 측정 명령도 포함되어야 합니다.
  • circuit에 대해 바인딩할 파라미터 값 컬렉션 θk\theta_k (런타임에 바인딩해야 하는 Parameter 객체가 사용된 경우에만 필요)
  • (선택적으로) circuit을 측정할 shots 수

다음 코드는 Sampler 프리미티브에 대한 벡터화된 입력 세트 예시를 보여주고, 이를 IBM® 백엔드에서 단일 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()

출력

하나 이상의 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:
[[ 0 0]
[ 3 255]
[ 0 0]
...
[ 3 255]
[ 2 255]
[ 3 255]]

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: {'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}

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:
[[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]]

결과 메타데이터

실행 결과 외에도, PrimitiveResultSamplerPubResult 객체 모두 제출된 작업에 대한 메타데이터 속성을 포함합니다. 모든 제출된 PUB에 대한 정보(다양한 런타임 옵션 등)를 포함하는 메타데이터는 PrimitiveResult.metadata에서, 각 PUB에 특정한 메타데이터는 SamplerPubResult.metadata에서 찾을 수 있습니다.

Sampler 결과 메타데이터에는 execution span이라고 하는 실행 타이밍 정보도 포함됩니다.

참고

메타데이터 필드에서 프리미티브 구현은 실행과 관련된 모든 정보를 반환할 수 있으며, 기본 프리미티브에 의해 보장되는 키-값 쌍은 없습니다. 따라서 반환된 메타데이터는 다른 프리미티브 구현에서 다를 수 있습니다.

# 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' : {},

실행 스팬 보기

Qiskit Runtime에서 실행된 SamplerV2 작업의 결과에는 메타데이터에 실행 타이밍 정보가 포함됩니다. 이 타이밍 정보는 특정 shots이 QPU에서 실행된 시간에 대한 상한 및 하한 타임스탬프 범위를 설정하는 데 사용할 수 있습니다. Shots는 ExecutionSpan 객체로 그룹화되며, 각각은 시작 시간, 종료 시간, 그리고 해당 스팬에서 수집된 shots의 사양을 나타냅니다.

실행 스팬은 ExecutionSpan.mask 메서드를 제공하여 해당 윈도우 동안 어떤 데이터가 실행되었는지를 지정합니다. 이 메서드는 임의의 Primitive Unified Block (PUB) 인덱스가 주어지면, 해당 윈도우 동안 실행된 모든 shots에 대해 True인 불리언 마스크를 반환합니다. PUB는 Sampler run 호출에 주어진 순서에 따라 인덱싱됩니다. 예를 들어, PUB의 형태가 (2, 3)이고 4 shots으로 실행된 경우, 마스크의 형태는 (2, 3, 4)입니다. 전체 세부 정보는 execution_span API 페이지를 참조하세요.

실행 스팬 정보를 보려면 SamplerV2가 반환한 결과의 메타데이터를 검토하는데, 이는 ExecutionSpans 객체 형태입니다. 이 객체는 SliceSpan과 같은 ExecutionSpan 서브클래스의 인스턴스를 포함하는 목록형 컨테이너입니다.

예시:

# 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)

실행 스팬은 특정 PUB와 관련된 정보를 포함하도록 필터링할 수 있으며, 인덱스로 선택합니다:

# 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>)])

실행 스팬 컬렉션에 대한 전역 정보를 봅니다:

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

특정 스팬을 추출하여 검사합니다:

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
참고

서로 다른 실행 스팬에 의해 지정된 타임 윈도우가 겹칠 수 있습니다. 이는 QPU가 동시에 여러 실행을 수행하고 있어서가 아니라, 양자 실행과 동시에 발생할 수 있는 특정 고전적 처리의 아티팩트입니다. 보장되는 것은 참조된 데이터가 보고된 실행 스팬에서 확실히 발생했다는 것이지만, 타임 윈도우의 한계가 가능한 한 타이트하다는 의미는 아닙니다.