Quantum Elements의 Qiskit Function인 Orbit으로 QFT+M 프로세스 충실도 벤치마킹
사용량 추정치: Heron r3 프로세서에서 2분. (참고: 이는 추정치일 뿐입니다. 실제 실행 시간은 다를 수 있습니다.) 기본적으로 이 튜토리얼은 세 개의 Orbit Function 작업을 하나의 IBM Quantum Compute Service 배치 모드 워크로드에 제출하며, 작업당 300개의 PUB로 총 900개의 PUB와 921,600 샷을 사용합니다.
경고: 동적 Circuit은 현재 실험적인 기능이며, Quantum Compute [3]의 제한 사항으로 인해 작업 실패가 발생할 수 있습니다. 예를 들어, 오류 6073은 작업이 고전 제어 하드웨어의 메모리 한도를 초과했음을 나타냅니다 [4]. 이 노트북은 하나의 배치 내에서 세 개의 Quantum Compute 작업으로 Circuit 크기를 분할하여 이러한 위험을 줄입니다 [5]. 각 고정 크기 비교는 하나의 작업에 유지되며, 큰 크기와 작은 크기는 작업의 고전 제어 워크로드를 균형 있게 조정하기 위해 짝지어집니다.
학습 목표
-
참고문헌 [1]의 그림 2a에서 샘플링된 프로세스 충실도 추정기가 사용하는 곱 상태 를 준비합니다.
-
양자 푸리에 변환 후 측정(QFT+M)의 동등한 유니터리 구현과 동적 구현을 만듭니다.
-
현재 캘리브레이션 및 연결성 데이터를 사용하여 동적 Circuit용 물리적 Qubit을 선택합니다.
-
Circuit 크기가 증가함에 따라 원시 유니터리, 원시 동적, Orbit 강화 동적 QFT+M 프로세스 충실도 추정치를 비교합니다.
-
mode="raw"및transpilation_mode="validate"를 사용하여 Orbit의 간소화된 트랜스파일 API를 사용합니다. -
각 고정 크기의 세 가지 전략 비교를 하나의 작업에 유지하면서 배치 모드 API를 통해 여러 Orbit 워크로드를 제출합니다.
-
동적 디커플링(DD)과 측정 오류 완화(MEM)가 적용되었는지 확인하기 위해 Orbit 메타데이터를 검사합니다.
배경
참고문헌 [1]의 그림 2a는 이상적인 QFT+M 채널의 프로세스 충실도를 노이즈가 있는 유니터리 구현 및 동적 구현과 비교하여 벤치마킹합니다. 샘플링된 계산 기저 레이블 에 대해, 이 벤치마크는 를 준비하고, 노이즈가 있는 QFT+M 구현을 적용한 다음, 해당하는 이상적인 출력을 얻을 확률 를 추정합니다. 이러한 역-QFT 상태는 분리 가능하며 하다마드 Gate와 가상 위상 회전으로 효율적으로 준비할 수 있습니다.
독립적으로 샘플링된 개의 레이블에 대해, 이 노트북은 참고문헌 [1]에서 유도된 비편향 추정기를 사용합니다:
동적 구성은 유니터리 QFT+M의 제어 위상 Gate를 회로 중간 측정과 고전적으로 조건화된 위상 회전으로 대체합니다 [1]. 지연된 측정을 통해 두 Circuit은 동일한 이상적인 출력 분포를 갖습니다. 동적 형태는 모든 Qubit 쌍 사이에 2-Qubit Gate가 필요하다는 요구 사항을 없애고, 대신 피드포워드를 통한 회로 중간 측정을 사용하며 연결성 제약이 없습니다. 측정과 피드포워드는 또한 아직 측정되지 않은 Qubit에 긴 유휴 기간을 남기므로, DD가 특히 중요해집니다.
그림 2a와의 관계. 이 노트북은 논문의 샘플링된 프로세스 충실도 프로토콜을 따르지만, 재현이 아니라 Orbit 중심의 튜토리얼 각색입니다. 예를 들어, 그림 2a는 2000 샷의 ibm_kyiv를 사용했지만, 우리는 QPU 시간을 절약하기 위해 최신 장치인 ibm_aachen을 1024 샷으로 더 적게 사용합니다.
예제 결과
아래의 정적 플롯은 ibm_aachen에서 아래 설명된 프로세스로 연속 실행된 세 개의 개발 작업의 평균 프로세스 충실도 곡선을 보여줍니다. 여기서 보여지듯이, Orbit은 동적 Circuit의 품질을 크게 향상시킬 수 있습니다. 동적 QFT는 발표된 벤치마크 품질과 일치하며 표준 유니터리 QFT보다 개선된 결과를 보여줍니다. 이후 살펴보겠지만, 이러한 결과는 자동화된 좋은 Qubit 선택, 자동 동적 디커플링 삽입(이 문제에 맞춰 수동으로 최적화되지 않음), 그리고 측정 오류 완화에서 비롯됩니다. 재미로, 특히 다른 Backend를 선택한 경우 이러한 결과를 마지막에 여러분의 결과와 비교해 보십시오.
참고: 이러한 결과는 Orbit을 사용한 이전의 성공적인 실행에 대한 예시적인 스냅샷이며, 성능 보장이 아닙니다. 아래 결과는 질적으로 유사해 보여야 하지만, 구체적인 내용은 실행 시점에 선택된 장치와 그 속성, 특히 측정 및 유휴 오류에 따라 달라집니다.
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-catalog qiskit-ibm-runtime
요구 사항
이 튜토리얼을 실행하기 전에 다음 패키지의 최신 버전을 설치하십시오:
-
numpy -
matplotlib -
qiskit -
qiskit-ibm-runtime -
qiskit-ibm-catalog
pip install qiskit qiskit-ibm-runtime qiskit-ibm-catalog numpy matplotlib
설정
IBM Quantum® Platform으로 인증하고, ibm_aachen을 로드하며, Qiskit Functions Catalog에서 Quantum Elements Orbit을 로드합니다. 기본 스윕은 15개의 Circuit 크기, 크기당 20개의 샘플링된 비트스트링, 그리고 세 가지 전략을 평가합니다. NUM_BATCH_JOBS=3은 하나의 배치 내에서 세 개의 작업으로 크기를 분할합니다. 개별 동적 Circuit 작업이 여전히 Backend의 고전 제어 메모리 한도에 도달하는 경우 N_VALUES 또는 M을 줄이거나 NUM_BATCH_JOBS를 늘리십시오. 목표가 Circuit의 수나 복잡성을 줄이는 것이 아니라 실행 사용량을 낮추는 것이라면 SHOTS를 줄이십시오.
import warnings
from collections import Counter, defaultdict
import matplotlib.pyplot as plt
import numpy as np
from qiskit import (
ClassicalRegister,
QuantumCircuit,
QuantumRegister,
transpile,
)
from qiskit.circuit import IfElseOp
from qiskit.synthesis.qft import synth_qft_full
from qiskit_ibm_catalog import QiskitFunctionsCatalog
from qiskit_ibm_runtime import Batch, QiskitRuntimeService
IBM_BACKEND_NAME = "ibm_aachen"
N_VALUES = [2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 35, 40]
M = 20
SHOTS = 1024
RNG_SEED = 12345
OPTIMIZATION_LEVEL = 0
NUM_BATCH_JOBS = 3
STRATEGY_LABELS = ("unitary/raw", "dynamic/raw", "dynamic/orbit")
def balanced_n_groups(
n_values: list[int], num_jobs: int = 3
) -> list[list[int]]:
values = sorted(n_values)
if len(set(values)) != len(values):
raise ValueError("N_VALUES must not contain duplicates")
if not 1 <= num_jobs <= len(values):
raise ValueError("NUM_BATCH_JOBS must be between 1 and len(N_VALUES)")
max_group_size = (len(values) + num_jobs - 1) // num_jobs
groups = [[] for _ in range(num_jobs)]
loads = [0] * num_jobs
pair_counts = [0] * num_jobs
remaining = values.copy()
while len(remaining) >= 2:
candidates = [
i
for i, group in enumerate(groups)
if len(group) + 2 <= max_group_size
]
if not candidates:
break
smallest = remaining.pop(0)
largest = remaining.pop()
job_index = min(
candidates, key=lambda i: (loads[i], len(groups[i]), i)
)
pair = (
[largest, smallest]
if pair_counts[job_index] % 2 == 0
else [smallest, largest]
)
groups[job_index].extend(pair)
loads[job_index] += smallest + largest
pair_counts[job_index] += 1
while remaining:
value = remaining.pop()
candidates = [
i for i, group in enumerate(groups) if len(group) < max_group_size
]
job_index = min(
candidates, key=lambda i: (loads[i], len(groups[i]), i)
)
groups[job_index].append(value)
loads[job_index] += value
return groups
N_GROUPS = balanced_n_groups(N_VALUES, NUM_BATCH_JOBS)
service = QiskitRuntimeService(channel="ibm_quantum_platform")
backend = service.backend(IBM_BACKEND_NAME)
if "if_else" not in backend.target.operation_names:
backend.target.add_instruction(IfElseOp, name="if_else")
catalog = QiskitFunctionsCatalog(channel="ibm_quantum_platform")
quantum_elements_orbit = catalog.load("quantum-elements/orbit")
if quantum_elements_orbit is None:
raise RuntimeError(
"Quantum Elements Orbit is not enabled for this IBM Quantum instance."
)
required_qubits = max(N_VALUES)
if backend.num_qubits < required_qubits:
raise ValueError(
f"Backend {backend.name} has {backend.num_qubits} qubits, "
f"but this benchmark needs at least {required_qubits}."
)
{
"backend": backend.name,
"num_qubits": backend.num_qubits,
"n_values": N_VALUES,
"m": M,
"shots": SHOTS,
"num_function_jobs": NUM_BATCH_JOBS,
"n_groups": N_GROUPS,
"pubs_per_job": [
len(group) * M * len(STRATEGY_LABELS) for group in N_GROUPS
],
"total_pubs": len(N_VALUES) * M * len(STRATEGY_LABELS),
"total_shots": len(N_VALUES) * M * len(STRATEGY_LABELS) * SHOTS,
}
qiskit_runtime_service._discover_account:WARNING:2026-07-21 15:57:39,310: Loading account with the given token. A saved account will not be used.
{'backend': 'ibm_aachen',
'num_qubits': 156,
'n_values': [2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 35, 40],
'm': 20,
'shots': 1024,
'num_function_jobs': 3,
'n_groups': [[40, 2, 7, 15, 10], [35, 3, 6, 20, 9], [30, 4, 5, 25, 8]],
'pubs_per_job': [300, 300, 300],
'total_pubs': 900,
'total_shots': 921600}
QFT+M Circuit 구축
샘플링된 각 정수 에 대해, bit_inv_qft는 하다마드에 이어 위상 회전을 사용하여 곱 상태 를 준비합니다. 그런 다음 노트북은 표준 유니터리 QFT 또는 그에 상응하는 반고전적 동적 QFT+M을 추가합니다.
두 구현 모두 최종 스왑 네트워크를 생략합니다. 따라서 Qiskit의 고전 비트 표시 순서로 인해 예상되는 측정 문자열은 의 0으로 채워진 이진 표현을 뒤집은 것이 되며, 이는 format(x, f"0{n}b")[::-1]로 인코딩됩니다.
def bit_inv_qft(circuit: QuantumCircuit, x: int, conv: str = "LSB") -> None:
num_qubits = circuit.num_qubits
circuit.h(range(num_qubits))
for j in range(num_qubits):
phase = (
2 * np.pi * x / 2 ** (num_qubits - j)
if conv == "LSB"
else 2 * np.pi * x / 2 ** (j + 1)
)
circuit.p(-phase, j)
def build_unitary_qft_circuit(num_qubits: int, x: int) -> QuantumCircuit:
if not 0 <= x < 2**num_qubits:
raise ValueError(
f"x={x} is outside the {num_qubits}-qubit basis range"
)
qreg = QuantumRegister(num_qubits, "q")
creg = ClassicalRegister(num_qubits, "c")
circuit = QuantumCircuit(qreg, creg, name=f"unitary_qft_{num_qubits}q")
bit_inv_qft(circuit, x)
circuit.append(
synth_qft_full(num_qubits, do_swaps=False), range(num_qubits)
)
circuit.measure(range(num_qubits), range(num_qubits))
return circuit
def _warn_if_precision_loss(max_num_entanglements: int) -> None:
if max_num_entanglements > -np.finfo(float).minexp:
warnings.warn(
"precision loss in QFT."
f" The rotation needed to represent {max_num_entanglements} entanglements"
" is smaller than the smallest normal floating-point number.",
category=RuntimeWarning,
stacklevel=4,
)
def synth_dynamic_qft(
circuit: QuantumCircuit, *, do_swaps: bool = False
) -> QuantumCircuit:
num_qubits = circuit.num_qubits
creg = circuit.cregs[0]
_warn_if_precision_loss(num_qubits - 1)
for j in reversed(range(num_qubits)):
circuit.h(j)
circuit.measure([j], [j])
if j > 0:
with circuit.if_test((creg[j], 1)):
for k in reversed(range(j)):
circuit.p(np.pi * (2.0 ** (k - j)), k)
if do_swaps:
for i in range(num_qubits // 2):
circuit.swap(i, num_qubits - i - 1)
return circuit
def build_dynamic_qft_circuit(num_qubits: int, x: int) -> QuantumCircuit:
if not 0 <= x < 2**num_qubits:
raise ValueError(
f"x={x} is outside the {num_qubits}-qubit basis range"
)
qreg = QuantumRegister(num_qubits, "q")
creg = ClassicalRegister(num_qubits, "c")
circuit = QuantumCircuit(qreg, creg, name=f"dynamic_qft_{num_qubits}q")
bit_inv_qft(circuit, x)
synth_dynamic_qft(circuit, do_swaps=False)
return circuit
def target_output_bitstring(x: int, n_qubits: int) -> str:
return format(int(x), f"0{n_qubits}b")[::-1]
def process_fidelity_from_success_probabilities(
success_probabilities: list[float],
) -> float:
m = len(success_probabilities)
if m <= 1:
raise ValueError(
"m must be larger than 1 for the process-fidelity estimator"
)
succ = np.asarray(success_probabilities, dtype=float)
return float(
(m / (m - 1)) * (np.mean(np.sqrt(succ)) ** 2)
- np.sum(succ) / (m * (m - 1))
)
동적 Circuit 물리적 Qubit 선택
동적 구현은 2-Qubit Gate가 필요하지 않으므로, 물리적 Qubit이 연결된 부분 그래프를 형성할 필요가 없습니다. 각 Circuit 크기에 대해, 선택기는 판독 오류가 낮을수록 80%, 과 가 높을수록 각각 10%씩 가중치를 둔 점수를 사용하여 현재 Backend Qubit의 순위를 매깁니다. 가능한 경우 먼저 직접적인 커플링이 없는 높은 점수의 Qubit을 선택하는데, 이는 최근접 이웃 크로스토크에 대한 노출을 줄일 수 있으며, 그런 다음 나머지 위치를 점수에 따라 채웁니다.
dynamic/raw와 dynamic/orbit 변형은 주어진 크기에 대해 정확히 동일한 선택된 레이아웃을 사용하므로, 두 변형의 비교는 레이아웃이 통제된 상태입니다. unitary/raw Circuit은 2-Qubit 연결성이 필요하기 때문에 대신 트랜스파일러에 의해 매핑되고 라우팅됩니다. 이 런타임 캘리브레이션 기반 선택은 이 튜토리얼에 특화된 것이며, 논문의 그림 2a 실험에 사용된 고정된 40-Qubit ibm_kyiv 레이아웃이 아닙니다.
def value_from_property(raw):
if raw is None:
return None
if isinstance(raw, tuple):
return raw[0]
return getattr(raw, "value", raw)
def qubit_property_value(properties, qubit: int, *names: str) -> float | None:
for name in names:
try:
value = value_from_property(
properties.qubit_property(qubit, name)
)
except Exception:
value = None
if value is not None:
return float(value)
return None
def measurement_error(properties, qubit: int) -> float | None:
readout = qubit_property_value(properties, qubit, "readout_error")
if readout is not None:
return readout
p01 = qubit_property_value(properties, qubit, "prob_meas0_prep1")
p10 = qubit_property_value(properties, qubit, "prob_meas1_prep0")
if p01 is not None and p10 is not None:
return 0.5 * (p01 + p10)
return None
def coupling_edges(backend) -> list[tuple[int, int]]:
coupling_map = getattr(backend, "coupling_map", None)
if coupling_map is not None:
try:
return [(int(a), int(b)) for a, b in coupling_map.get_edges()]
except Exception:
pass
built = backend.target.build_coupling_map()
return [(int(a), int(b)) for a, b in built.get_edges()]
def neighbor_map(backend) -> dict[int, set[int]]:
neighbors = {qubit: set() for qubit in range(backend.num_qubits)}
for a, b in coupling_edges(backend):
neighbors[a].add(b)
neighbors[b].add(a)
return neighbors
def anchored_score(
value: float | None, *, good: float, bad: float, higher_is_better: bool
) -> float:
if value is None:
return 0.0
if higher_is_better:
low, high = sorted((bad, good))
score = (value - low) / (high - low)
else:
low, high = sorted((good, bad))
score = (high - value) / (high - low)
return float(min(1.0, max(0.0, score)))
def qubit_metrics(backend) -> list[dict]:
properties = backend.properties()
rows = []
for qubit in range(backend.num_qubits):
t1 = qubit_property_value(properties, qubit, "T1", "t1")
t2 = qubit_property_value(properties, qubit, "T2", "t2")
meas_error = measurement_error(properties, qubit)
measurement_score = anchored_score(
meas_error, good=0.005, bad=0.05, higher_is_better=False
)
t1_score = anchored_score(
t1, good=0.00025, bad=0.00005, higher_is_better=True
)
t2_score = anchored_score(
t2, good=0.00025, bad=0.00005, higher_is_better=True
)
rows.append(
{
"qubit": qubit,
"t1": t1,
"t2": t2,
"measurement_error": meas_error,
"score": 0.8 * measurement_score
+ 0.1 * t1_score
+ 0.1 * t2_score,
}
)
return sorted(rows, key=lambda row: row["score"], reverse=True)
def select_dynamic_qubits(backend, n_qubits: int) -> list[int]:
ranked = qubit_metrics(backend)
neighbors = neighbor_map(backend)
selected = []
blocked = set()
for row in ranked:
qubit = row["qubit"]
if qubit in blocked:
continue
selected.append(qubit)
blocked.add(qubit)
blocked.update(neighbors.get(qubit, set()))
if len(selected) == n_qubits:
return selected
for row in ranked:
qubit = row["qubit"]
if qubit not in selected:
selected.append(qubit)
if len(selected) == n_qubits:
return selected
raise RuntimeError(f"Could not select {n_qubits} physical qubits")
print("The top 3 qubits (according to our scoring): ")
print(qubit_metrics(backend)[0:3])
print("Worst 3 qubits (according to our scoring): ")
print(qubit_metrics(backend)[-3:])
The top 3 qubits (according to our scoring):
[{'qubit': 0, 't1': 0.0002514242577986401, 't2': 0.00037559012475638467, 'measurement_error': 0.0028076171875, 'score': 1.0}, {'qubit': 20, 't1': 0.0002526252407383437, 't2': 0.00038493543771861573, 'measurement_error': 0.00390625, 'score': 1.0}, {'qubit': 25, 't1': 0.0002712841332005567, 't2': 0.00025793268824583597, 'measurement_error': 0.0040283203125, 'score': 1.0}]
Worst 3 qubits (according to our scoring):
[{'qubit': 146, 't1': 7.619772882181663e-05, 't2': 0.00014166983578724752, 'measurement_error': 0.0802001953125, 'score': 0.05893378230453208}, {'qubit': 51, 't1': 0.00014200221819602618, 't2': 1.930870507157441e-06, 'measurement_error': 0.054443359375, 'score': 0.04600110909801309}, {'qubit': 35, 't1': 7.116087485472031e-05, 't2': 9.463696242928626e-05, 'measurement_error': 0.14501953125, 'score': 0.03289891864200328}]
벤치마크 PUB 준비
모든 (N, x) 쌍에 대해 노트북은 먼저 논리 회로를 트랜스파일하고 각 전략에 대해 하나의 Sampler PUB를 생성합니다:
-
unitary/raw: Orbit DD나 MEM 없이, 트랜스파일러가 선택한 레이아웃에서 필요에 따라 라우팅을 수행하는 유니터리 QFT+M입니다. -
dynamic/raw: Orbit DD나 MEM 없이, 캘리브레이션으로 선택된 물리 큐비트에서 실행되는 동적 QFT+M입니다. -
dynamic/orbit: Orbit DD와 MEM이 활성화된, 동일한 물리 큐비트에서 실행되는 동일한 트랜스파일된 동적 회로입니다.
Orbit 강화 PUB는 매핑이 이미 선택되었기 때문에 transpilation_mode="validate"를 사용합니다. Orbit는 제공된 물리 회로를 다시 매핑하는 대신 검증한 후, DD 및 MEM 파이프라인을 적용합니다. 강화된 동적 곡선만 MEM을 요청하므로, 이를 독립적인 DD 대 비-DD 비교로 해석해서는 안 됩니다.
PUB, PUB별 옵션, 결과 레코드는 배치 작업 인덱스별로 저장됩니다. 각 고정된 에 대해 unitary/raw, dynamic/raw, dynamic/orbit PUB는 동일한 작업 내에 함께 유지됩니다. 그룹화 헬퍼는 큰 회로 크기와 작은 회로 크기를 짝짓고, 그 순서를 교대하며, 고전 제어 워크로드의 간단한 대리 지표로 세 작업 간 의 합을 균형 있게 조정합니다.
strategy_options = {
"unitary/raw": {"mode": "raw"},
"dynamic/raw": {"mode": "raw"},
"dynamic/orbit": {"mode": "orbit", "transpilation_mode": "validate"},
}
rng = np.random.default_rng(RNG_SEED)
pubs_by_job = [[] for _ in N_GROUPS]
pub_options_by_job = [[] for _ in N_GROUPS]
pub_records_by_job = [[] for _ in N_GROUPS]
layout_summary = {}
target_decimals_by_n = {
n_qubits: [int(x) for x in rng.integers(0, 2**n_qubits, size=M)]
for n_qubits in N_VALUES
}
for job_index, n_group in enumerate(N_GROUPS):
for n_qubits in n_group:
dynamic_qubits = select_dynamic_qubits(backend, n_qubits)
layout_summary[str(n_qubits)] = {"dynamic_qubits": dynamic_qubits}
for x in target_decimals_by_n[n_qubits]:
target_bitstring = target_output_bitstring(x, n_qubits)
unitary_logical = build_unitary_qft_circuit(n_qubits, x)
dynamic_logical = build_dynamic_qft_circuit(n_qubits, x)
unitary_transpiled = transpile(
unitary_logical,
backend=backend,
optimization_level=OPTIMIZATION_LEVEL,
seed_transpiler=RNG_SEED,
)
dynamic_transpiled = transpile(
dynamic_logical,
backend=backend,
optimization_level=OPTIMIZATION_LEVEL,
seed_transpiler=RNG_SEED,
initial_layout=dynamic_qubits,
)
circuits_by_label = {
"unitary/raw": unitary_transpiled,
"dynamic/raw": dynamic_transpiled,
"dynamic/orbit": dynamic_transpiled,
}
for label in STRATEGY_LABELS:
circuit = circuits_by_label[label]
options = dict(strategy_options[label])
pubs_by_job[job_index].append((circuit, None, SHOTS))
pub_options_by_job[job_index].append(options)
pub_records_by_job[job_index].append(
{
"job_index": job_index,
"n_qubits": n_qubits,
"target_decimal": x,
"target_bitstring": target_bitstring,
"label": label,
"pub_options": options,
"dynamic_qubits": (
dynamic_qubits
if label.startswith("dynamic/")
else None
),
"transpiled_depth": circuit.depth(),
"transpiled_size": circuit.size(),
}
)
{
"num_function_jobs": len(N_GROUPS),
"n_groups": {
job_index: group for job_index, group in enumerate(N_GROUPS)
},
"n_load_per_job": {
job_index: sum(group) for job_index, group in enumerate(N_GROUPS)
},
"pubs_per_job": {
job_index: len(pubs) for job_index, pubs in enumerate(pubs_by_job)
},
"expected_executions_per_job": {
job_index: len(pubs) * SHOTS
for job_index, pubs in enumerate(pubs_by_job)
},
"first_pub_record_by_job": {
job_index: records[0]
for job_index, records in enumerate(pub_records_by_job)
},
"largest_dynamic_qubit_set": layout_summary[str(max(N_VALUES))][
"dynamic_qubits"
],
}
{'num_function_jobs': 3,
'n_groups': {0: [40, 2, 7, 15, 10],
1: [35, 3, 6, 20, 9],
2: [30, 4, 5, 25, 8]},
'n_load_per_job': {0: 74, 1: 73, 2: 72},
'pubs_per_job': {0: 300, 1: 300, 2: 300},
'expected_executions_per_job': {0: 307200, 1: 307200, 2: 307200},
'first_pub_record_by_job': {0: {'job_index': 0,
'n_qubits': 40,
'target_decimal': 853235401719,
'target_bitstring': '1110111111000000110100110001010101100011',
'label': 'unitary/raw',
'pub_options': {'mode': 'raw'},
'dynamic_qubits': None,
'transpiled_depth': 4778,
'transpiled_size': 28259},
1: {'job_index': 1,
'n_qubits': 35,
'target_decimal': 26888951661,
'target_bitstring': '10110110111010110010110101000010011',
'label': 'unitary/raw',
'pub_options': {'mode': 'raw'},
'dynamic_qubits': None,
'transpiled_depth': 3614,
'transpiled_size': 21204},
2: {'job_index': 2,
'n_qubits': 30,
'target_decimal': 620442965,
'target_bitstring': '101010101010110011011111001001',
'label': 'unitary/raw',
'pub_options': {'mode': 'raw'},
'dynamic_qubits': None,
'transpiled_depth': 2835,
'transpiled_size': 15078}},
'largest_dynamic_qubit_set': [0,
20,
25,
27,
33,
59,
74,
80,
95,
144,
151,
155,
79,
90,
60,
68,
114,
107,
13,
126,
133,
103,
3,
87,
53,
41,
130,
5,
98,
135,
153,
15,
116,
45,
7,
48,
136,
11,
147,
77]}
벤치마크 실행
배치를 하나 생성한 다음, 그 안에 세 개의 Orbit 함수 작업을 제출합니다.
작업은 큐비트 수 그룹으로 분할되어 고정된 에 대한 세 가지 전략을 모두 비교할 수 있습니다. 즉, 고정된 에 대한 모든 60개의 PUB — 20개의 샘플링된 입력에 세 가지 전략을 곱한 값 — 이 동일한 작업 내에서 실행되어 최대한 공정하게 비교될 수 있습니다(그렇지 않고 다른 작업에서 실행되면, 대기 중에 디바이스가 드리프트할 수 있습니다). 기본 그룹은 큰 회로와 작은 회로를 결합하며 각각 300개의 PUB를 포함하여, 하나의 작업에 가장 큰 동적 프로그램이 모두 몰릴 가능성을 줄이는 동시에 작업 내 비교를 유지합니다.
runtime_batch = Batch(backend=backend)
jobs = []
try:
for job_index, pubs in enumerate(pubs_by_job):
jobs.append(
quantum_elements_orbit.run(
primitive="sampler",
pubs=pubs,
backend_name=backend.name,
options={
"pub_options": pub_options_by_job[job_index],
"save_backend_info": True,
},
)
)
except Exception:
runtime_batch.close()
raise
{
"runtime_batch_id": runtime_batch.session_id,
"jobs": {
job_index: {
"backend": backend.name,
"function_job_id": job.job_id,
"status": job.status(),
"n_values": N_GROUPS[job_index],
"num_pubs": len(pubs_by_job[job_index]),
}
for job_index, job in enumerate(jobs)
},
}
{'runtime_batch_id': '80120e36-436d-46c7-96c9-597ec86060c1',
'jobs': {0: {'backend': 'ibm_aachen',
'function_job_id': '2b6b05ac-0136-40f0-94bf-ade5658f5f4f',
'status': 'QUEUED',
'n_values': [40, 2, 7, 15, 10],
'num_pubs': 300},
1: {'backend': 'ibm_aachen',
'function_job_id': '4f046fd4-80e4-460b-87c7-e7252691f764',
'status': 'QUEUED',
'n_values': [35, 3, 6, 20, 9],
'num_pubs': 300},
2: {'backend': 'ibm_aachen',
'function_job_id': '6d70d64d-fa38-4ca2-9cbd-ffda5d8c99be',
'status': 'QUEUED',
'n_values': [30, 4, 5, 25, 8],
'num_pubs': 300}}}
결과 검색 및 프로세스 충실도 계산
각 큐비트 그룹 결과를 독립적으로 검색하고 검증한 다음, 작업 인덱스가 지정된 레코드를 통해 세 작업 스트림을 병합합니다. 배치는 모든 함수 결과가 요청되는 동안 열려 있으며, 모든 작업이 시도된 후 finally 블록에서 닫힙니다. 각 PUB에 대해 는 예상 비트스트링에 할당된 확률입니다. extract_counts는 호출자에게 반환된 카운트를 읽습니다; dynamic/orbit의 경우, 완화가 성공하면 이는 MEM으로 조정된 카운트입니다. extract_raw_counts는 또한 Orbit 메타데이터에 기록된 해당 완화되지 않은 카운트를 복구합니다. 코드는 각 (N, label) 쌍에 대해 20개의 값을 그룹화하고 위에서 소개한 추정기를 적용합니다.
따라서 그려진 process_fidelity 딕셔너리는 unitary/raw와 dynamic/raw에는 원시 카운트를 사용하지만, dynamic/orbit에는 MEM으로 조정된 카운트를 사용합니다. 병렬 raw_process_fidelity 딕셔너리는 모든 전략에 대해 완화되지 않은 계산을 유지하며, MEM의 효과를 나머지 Orbit 파이프라인과 분리할 때 유용합니다. MEM은 반환된 출력 히스토그램을 보정하지만, 실시간 피드포워드가 이미 사용한 중간 회로 측정 결과를 소급하여 변경할 수는 없습니다.
def extract_counts(pub_result) -> dict[str, int]:
data = getattr(pub_result, "data", None)
if data is None:
raise TypeError("pub_result.data is missing")
for name in dir(data):
if name.startswith("_"):
continue
register = getattr(data, name)
get_counts = getattr(register, "get_counts", None)
if callable(get_counts):
counts = get_counts()
if counts:
return counts
raise TypeError(
"No classical register with get_counts() found in pub_result.data"
)
def extract_raw_counts(pub_result) -> dict[str, int]:
orbit_metadata = pub_result.metadata.get("quantum_elements_orbit", {})
mem_report = orbit_metadata.get("measurementErrorMitigation", {})
return mem_report.get("rawCounts") or extract_counts(pub_result)
def probability_for_bitstring(
counts: dict[str, int], bitstring: str, n_qubits: int
) -> float:
total = sum(counts.values())
if total <= 0:
return 0.0
normalized = Counter()
for measured, count in counts.items():
key = measured.replace(" ", "")[-n_qubits:].zfill(n_qubits)
normalized[key] += count
return float(normalized.get(bitstring, 0) / total)
results_by_job = {}
job_failures = []
try:
for job_index, job in enumerate(jobs):
try:
job_result = job.result()
except Exception as exc:
job_logs = getattr(job, "logs", lambda: "")()
if job_logs:
print(f"Logs for job {job_index} ({job.job_id}):\n{job_logs}")
job_failures.append(
f"job {job_index} ({job.job_id}) failed: {type(exc).__name__}: {exc}"
)
continue
expected_results = len(pub_records_by_job[job_index])
if len(job_result) != expected_results:
job_failures.append(
f"job {job_index} ({job.job_id}) returned {len(job_result)} PUB results; "
f"expected {expected_results}"
)
continue
results_by_job[job_index] = job_result
finally:
runtime_batch.close()
if job_failures:
raise RuntimeError(
"One or more batched Orbit jobs failed:\n" + "\n".join(job_failures)
)
grouped_success = defaultdict(list)
grouped_raw_success = defaultdict(list)
pub_summaries = []
for job_index, job_result in sorted(results_by_job.items()):
records = pub_records_by_job[job_index]
for record, pub_result in zip(records, job_result, strict=True):
label = record["label"]
n_qubits = record["n_qubits"]
counts = extract_counts(pub_result)
raw_counts = extract_raw_counts(pub_result)
success = probability_for_bitstring(
counts, record["target_bitstring"], n_qubits
)
raw_success = probability_for_bitstring(
raw_counts, record["target_bitstring"], n_qubits
)
key = (n_qubits, label)
grouped_success[key].append(success)
grouped_raw_success[key].append(raw_success)
orbit_report = pub_result.metadata.get("quantum_elements_orbit", {})
mem_report = orbit_report.get("measurementErrorMitigation", {})
pub_summaries.append(
{
**record,
"function_job_id": jobs[job_index].job_id,
"runtime_batch_id": runtime_batch.session_id,
"success_probability": success,
"raw_success_probability": raw_success,
"orbit_mode": orbit_report.get("mode"),
"transpilation_mode": orbit_report.get("transpilationMode"),
"physical_layout": orbit_report.get("physicalLayout"),
"dd_status": orbit_report.get("status", "not_applied"),
"num_sequences_added": orbit_report.get(
"numSequencesAdded", 0
),
"num_gaps_filled": orbit_report.get("numGapsFilled", 0),
"dynamic_dd_seq": orbit_report.get("dynamicDdSeq"),
"mem_status": mem_report.get("status", "not_requested"),
"warnings": orbit_report.get("warnings", [])
+ mem_report.get("warnings", []),
}
)
process_fidelity = defaultdict(dict)
raw_process_fidelity = defaultdict(dict)
mean_success_probability = defaultdict(dict)
raw_mean_success_probability = defaultdict(dict)
for (n_qubits, label), probabilities in sorted(grouped_success.items()):
n_key = str(n_qubits)
process_fidelity[n_key][label] = (
process_fidelity_from_success_probabilities(probabilities)
)
mean_success_probability[n_key][label] = float(np.mean(probabilities))
for (n_qubits, label), probabilities in sorted(grouped_raw_success.items()):
n_key = str(n_qubits)
raw_process_fidelity[n_key][label] = (
process_fidelity_from_success_probabilities(probabilities)
)
raw_mean_success_probability[n_key][label] = float(np.mean(probabilities))
process_fidelity = dict(process_fidelity)
raw_process_fidelity = dict(raw_process_fidelity)
mean_success_probability = dict(mean_success_probability)
raw_mean_success_probability = dict(raw_mean_success_probability)
{
"runtime_batch_id": runtime_batch.session_id,
"function_job_ids": {
job_index: job.job_id for job_index, job in enumerate(jobs)
},
"n_groups": {
job_index: group for job_index, group in enumerate(N_GROUPS)
},
"process_fidelity": process_fidelity,
"mean_success_probability": mean_success_probability,
}
{'runtime_batch_id': '80120e36-436d-46c7-96c9-597ec86060c1',
'function_job_ids': {0: '2b6b05ac-0136-40f0-94bf-ade5658f5f4f',
1: '4f046fd4-80e4-460b-87c7-e7252691f764',
2: '6d70d64d-fa38-4ca2-9cbd-ffda5d8c99be'},
'n_groups': {0: [40, 2, 7, 15, 10],
1: [35, 3, 6, 20, 9],
2: [30, 4, 5, 25, 8]},
'process_fidelity': {'2': {'dynamic/orbit': 0.9870551835473073,
'dynamic/raw': 0.9912537998030566,
'unitary/raw': 0.9884650767434809},
'3': {'dynamic/orbit': 0.9662998634131841,
'dynamic/raw': 0.9699631603283018,
'unitary/raw': 0.9388637172865901},
'4': {'dynamic/orbit': 0.9271266520750502,
'dynamic/raw': 0.7334377020091254,
'unitary/raw': 0.9010122207121433},
'5': {'dynamic/orbit': 0.8883501513887149,
'dynamic/raw': 0.6577660260669806,
'unitary/raw': 0.7806443417987445},
'6': {'dynamic/orbit': 0.8524225652033044,
'dynamic/raw': 0.4444025126308521,
'unitary/raw': 0.7167426842521228},
'7': {'dynamic/orbit': 0.832962085697061,
'dynamic/raw': 0.2253787798698553,
'unitary/raw': 0.5746335601063436},
'8': {'dynamic/orbit': 0.7881895956180588,
'dynamic/raw': 0.16909516699831612,
'unitary/raw': 0.5408263851227074},
'9': {'dynamic/orbit': 0.7422635627368794,
'dynamic/raw': 0.0242474245097341,
'unitary/raw': 0.4855953298367578},
'10': {'dynamic/orbit': 0.7002274273149545,
'dynamic/raw': 0.033718865729016285,
'unitary/raw': 0.3607634828181049},
'15': {'dynamic/orbit': 0.4694995355699914,
'dynamic/raw': 7.70970394736842e-05,
'unitary/raw': 0.054582117352985286},
'20': {'dynamic/orbit': 0.24118032284867608,
'dynamic/raw': 4.235164736271502e-22,
'unitary/raw': 0.0},
'25': {'dynamic/orbit': 0.027122712989729438,
'dynamic/raw': 0.0,
'unitary/raw': 0.0},
'30': {'dynamic/orbit': 0.0003581886014704875,
'dynamic/raw': 0.0,
'unitary/raw': 0.0},
'35': {'dynamic/orbit': 0.0, 'dynamic/raw': 0.0, 'unitary/raw': 0.0},
'40': {'dynamic/orbit': 0.0, 'dynamic/raw': 0.0, 'unitary/raw': 0.0}},
'mean_success_probability': {'2': {'dynamic/orbit': 0.987060546875,
'dynamic/raw': 0.991259765625,
'unitary/raw': 0.9884765625},
'3': {'dynamic/orbit': 0.96630859375,
'dynamic/raw': 0.969970703125,
'unitary/raw': 0.939013671875},
'4': {'dynamic/orbit': 0.9271484375,
'dynamic/raw': 0.7337890625,
'unitary/raw': 0.901318359375},
'5': {'dynamic/orbit': 0.88837890625,
'dynamic/raw': 0.657861328125,
'unitary/raw': 0.78115234375},
'6': {'dynamic/orbit': 0.85244140625,
'dynamic/raw': 0.44453125,
'unitary/raw': 0.71728515625},
'7': {'dynamic/orbit': 0.8330078125,
'dynamic/raw': 0.22568359375,
'unitary/raw': 0.575390625},
'8': {'dynamic/orbit': 0.788232421875,
'dynamic/raw': 0.169189453125,
'unitary/raw': 0.541796875},
'9': {'dynamic/orbit': 0.742333984375,
'dynamic/raw': 0.0244140625,
'unitary/raw': 0.487353515625},
'10': {'dynamic/orbit': 0.70029296875,
'dynamic/raw': 0.033935546875,
'unitary/raw': 0.363037109375},
'15': {'dynamic/orbit': 0.4697265625,
'dynamic/raw': 0.00029296875,
'unitary/raw': 0.055615234375},
'20': {'dynamic/orbit': 0.241357421875,
'dynamic/raw': 4.8828125e-05,
'unitary/raw': 0.0},
'25': {'dynamic/orbit': 0.041015625, 'dynamic/raw': 0.0, 'unitary/raw': 0.0},
'30': {'dynamic/orbit': 0.0013671875,
'dynamic/raw': 0.0,
'unitary/raw': 0.0},
'35': {'dynamic/orbit': 0.0, 'dynamic/raw': 0.0, 'unitary/raw': 0.0},
'40': {'dynamic/orbit': 0.0, 'dynamic/raw': 0.0, 'unitary/raw': 0.0}}}
Orbit DD 메타데이터 검사
아래 요약은 요청된 DD가 삽입되었다고 가정하는 대신 dynamic/orbit PUB 메타데이터를 확인합니다. 상태, 보고된 동적 DD 시퀀스, 경고, 그리고 채워진 갭과 추가된 시퀀스의 수를 검사합니다. 성공적인 삽입은 적어도 일부 PUB에 대해 0이 아닌 카운트를 생성해야 하지만, 정확한 값은 예약된 회로, 백엔드 타이밍 제약 조건, 회로 크기에 따라 달라집니다. 이 메타데이터는 Orbit가 적용한 시퀀스를 설명하는 것으로, 보고서가 그 동등성을 명시적으로 입증하지 않는 한 논문의 FC-DD 프로토콜로 표시해서는 안 됩니다.
dd_summary = defaultdict(lambda: Counter())
sequence_totals = defaultdict(int)
warning_examples = []
for summary in pub_summaries:
if summary["label"] != "dynamic/orbit":
continue
n_key = str(summary["n_qubits"])
dd_summary[n_key][summary["dd_status"]] += 1
sequence_totals[n_key] += int(summary.get("num_sequences_added") or 0)
if summary.get("warnings") and len(warning_examples) < 5:
warning_examples.append(
{
"n_qubits": summary["n_qubits"],
"target_decimal": summary["target_decimal"],
"warnings": summary["warnings"],
}
)
{
"dynamic_orbit_dd_status_counts": {
key: dict(value) for key, value in dd_summary.items()
},
"dynamic_orbit_sequences_added": dict(sequence_totals),
"warning_examples": warning_examples,
}
{'dynamic_orbit_dd_status_counts': {'40': {'dd_inserted': 20},
'2': {'dd_inserted': 20},
'7': {'dd_inserted': 20},
'15': {'dd_inserted': 20},
'10': {'dd_inserted': 20},
'35': {'dd_inserted': 20},
'3': {'dd_inserted': 20},
'6': {'dd_inserted': 20},
'20': {'dd_inserted': 20},
'9': {'dd_inserted': 20},
'30': {'dd_inserted': 20},
'4': {'dd_inserted': 20},
'5': {'dd_inserted': 20},
'25': {'dd_inserted': 20},
'8': {'dd_inserted': 20}},
'dynamic_orbit_sequences_added': {'40': 31200,
'2': 40,
'7': 840,
'15': 4200,
'10': 1800,
'35': 23800,
'3': 120,
'6': 600,
'20': 7600,
'9': 1440,
'30': 17400,
'4': 240,
'5': 400,
'25': 12000,
'8': 1120},
'warning_examples': [{'n_qubits': 40,
'target_decimal': 853235401719,
'warnings': ['Post-DD x/y pulses are rewritten into the target basis (e.g. sx) for ISA compliance; this shifts exact pulse timing within each gap, so DD spacing is approximate. A future release will emit native pulses directly.',
'MEM was applied unconditionally to the returned output bitstring without inferring whether each bit came from a terminal measurement or a mid-circuit measurement. This is meaningful for bits intended as circuit outputs, but Orbit does not retroactively or in real time change conditional branches that used unmitigated measurement results.']},
{'n_qubits': 40,
'target_decimal': 954673909846,
'warnings': ['Post-DD x/y pulses are rewritten into the target basis (e.g. sx) for ISA compliance; this shifts exact pulse timing within each gap, so DD spacing is approximate. A future release will emit native pulses directly.',
'MEM was applied unconditionally to the returned output bitstring without inferring whether each bit came from a terminal measurement or a mid-circuit measurement. This is meaningful for bits intended as circuit outputs, but Orbit does not retroactively or in real time change conditional branches that used unmitigated measurement results.']},
{'n_qubits': 40,
'target_decimal': 524641045908,
'warnings': ['Post-DD x/y pulses are rewritten into the target basis (e.g. sx) for ISA compliance; this shifts exact pulse timing within each gap, so DD spacing is approximate. A future release will emit native pulses directly.',
'MEM was applied unconditionally to the returned output bitstring without inferring whether each bit came from a terminal measurement or a mid-circuit measurement. This is meaningful for bits intended as circuit outputs, but Orbit does not retroactively or in real time change conditional branches that used unmitigated measurement results.']},
{'n_qubits': 40,
'target_decimal': 185651043478,
'warnings': ['Post-DD x/y pulses are rewritten into the target basis (e.g. sx) for ISA compliance; this shifts exact pulse timing within each gap, so DD spacing is approximate. A future release will emit native pulses directly.',
'MEM was applied unconditionally to the returned output bitstring without inferring whether each bit came from a terminal measurement or a mid-circuit measurement. This is meaningful for bits intended as circuit outputs, but Orbit does not retroactively or in real time change conditional branches that used unmitigated measurement results.']},
{'n_qubits': 40,
'target_decimal': 587114273567,
'warnings': ['Post-DD x/y pulses are rewritten into the target basis (e.g. sx) for ISA compliance; this shifts exact pulse timing within each gap, so DD spacing is approximate. A future release will emit native pulses directly.',
'MEM was applied unconditionally to the returned output bitstring without inferring whether each bit came from a terminal measurement or a mid-circuit measurement. This is meaningful for bits intended as circuit outputs, but Orbit does not retroactively or in real time change conditional branches that used unmitigated measurement results.']}]}
프로세스 충실도 곡선 그리기
이 플롯은 세 가지 전략에 대한 큐비트 수 대비 샘플링된 QFT+M 프로세스 충실도 점 추정치를 보여줍니다. dynamic/raw와 dynamic/orbit는 각 크기에서 물리적 레이아웃을 공유합니다; unitary/raw는 트랜스파일러의 레이아웃과 라우팅을 사용합니다.
그림 2a와 달리, 이 플롯은 유니터리-with-DD 곡선이나 불확실성 밴드를 보여주지 않으며, 그 원시 곡선은 판독 완화가 적용되지 않았습니다. 이는 이 Orbit 워크플로에 대한 그림 2a 스타일의 스케일링 비교로 읽는 것이 가장 적절하며, 게재된 곡선의 직접적인 재현으로 읽어서는 안 됩니다.
from datetime import datetime
from zoneinfo import ZoneInfo
closed_at = runtime_batch.details()["closed_at"] # "2026-07-22T00:08:54.89Z"
closed_dt = datetime.fromisoformat(closed_at.replace("Z", "+00:00"))
closed_local = closed_dt.astimezone(ZoneInfo("America/Los_Angeles"))
labels = ["dynamic/orbit", "dynamic/raw", "unitary/raw"]
colors = {
"dynamic/orbit": "#26735b",
"dynamic/raw": "#9b1c31",
"unitary/raw": "#6e6e6e",
}
pretty_labels = {
"dynamic/orbit": "Dynamic QFT+M with Orbit",
"dynamic/raw": "Dynamic QFT+M",
"unitary/raw": "Unitary QFT+M",
}
series = []
for label in labels:
values = [process_fidelity[str(n)][label] for n in N_VALUES]
log_values = [value if value > 0.0 else float("nan") for value in values]
series.append((label, values, log_values))
nonzero_values = [
value
for _, _, log_values in series
for value in log_values
if value > 0.0
]
if not nonzero_values:
raise RuntimeError(
"No nonzero process-fidelity values found for log inset"
)
log_floor = min(nonzero_values) / 2
fig, ax = plt.subplots(figsize=(9.8, 5.6))
for label, values, _ in series:
ax.plot(
N_VALUES,
values,
marker="o",
linewidth=2.0,
markersize=5,
color=colors[label],
label=pretty_labels[label],
)
ax.set_xlabel("N qubits")
ax.set_ylabel("Process fidelity")
finished_time_for_title = globals().get("finished_local", closed_local)
ax.set_title(
f"Dynamic QFT Orbit results on {IBM_BACKEND_NAME}\n"
f"Job finished {finished_time_for_title:%Y-%m-%d %H:%M %Z}"
)
ax.set_xticks(N_VALUES)
ax.set_ylim(bottom=0)
ax.grid(axis="both", alpha=0.25)
ax.legend(loc="upper right")
inset = ax.inset_axes([0.53, 0.31, 0.44, 0.43])
for label, _, log_values in series:
inset.plot(
N_VALUES,
log_values,
marker="o",
linewidth=2.0,
markersize=5,
color=colors[label],
)
inset.set_yscale("log")
inset.set_ylim(bottom=log_floor)
inset.set_xlim(min(N_VALUES), max(N_VALUES))
inset.set_title("Log scale; zeros omitted", fontsize=9)
inset.grid(axis="both", alpha=0.25)
inset.tick_params(axis="both", labelsize=8)
inset.patch.set_alpha(0.96)
fig.tight_layout()
plt.show()
