해밀턴 시뮬레이션 회로용 컴파일 방법
예상 소요 시간: IBM Heron 프로세서 기준 1분 미만 (참고: 이는 단지 예상치일 뿐입니다.) (실행 시간은 다를 수 있습니다.)
학습 성과
- SABRE와 함께 Qiskit 트랜스파일러를 사용하여 레이아웃 및 라우팅 최적화를 수행하는 방법
- AI 기반 트랜스파일러를 활용하여 회로를 고도로 최적화하는 방법
- 해밀턴 연산 회로에서 연산을 합성하기
PauliEvolutionGate위해 Rustiq 플러그인을 사용하는 방법 - 2-큐비트 깊이, 총 게이트 수 및 실행 시간을 활용하여 컴파일 방법을 벤치마킹하고 비교하는 방법
전제조건
배경
양자 회로 컴파일링은 고수준 양자 알고리즘을 대상 하드웨어의 제약 조건을 준수하는 물리적 회로로 변환합니다. 효율적인 컴파일링은 회로 깊이와 게이트 수를 크게 줄일 수 있으며, 이 두 가지 요소는 모두 단기적인 양자 장치에서 얻어지는 결과의 품질에 직접적인 영향을 미칩니다.
PauliEvolutionGate이 튜토리얼에서는 로 구축된 해밀토니안 시뮬레이션 회로에 대해 세 가지 컴파일 방법을 벤치마킹합니다. 이러한 회로는 쌍을 이루는 큐비트 간의 상호작용( 항, 항, 항 등)을 모델링하며, 양자화학, 응집물질 물리학, 재료과학 분야에서 흔히 사용됩니다.
벤치마크 회로는 Benchpress 저장소를 통해 접근할 수 있는 Hamlib 컬렉션에서 가져온 것입니다. Hamlib은 표준화된 대표적인 해밀토니안 집합을 제공하여, 실제 시뮬레이션 워크로드에 대한 컴파일 전략을 비교할 수 있게 해줍니다.
컴파일 방법 개요
SABRE를 지원하는 Qiskit 트랜스파일러
Qiskit 트랜스파일러는 SABRE(SWAP 기반 BidiREctional 휴리스틱 탐색) 알고리즘을 사용하여 회로 레이아웃과 라우팅을 최적화합니다. SABRE는 하드웨어 연결 제약 조건을 준수하면서 SWAP 게이트 수와 이로 인한 회로 깊이 증가를 최소화하는 데 중점을 둡니다. 이 방법은 성능과 컴파일 시간 사이에서 적절한 균형을 제공하는 범용적인 방법입니다. 자세한 내용은 [1] 을 참조하십시오. SABRE의 장점과 매개변수 탐색에 대해서는 별도의 튜 토리얼에서 심도 있게 다루고 있습니다.
AI 기반 트랜스파일러
이 AI 기반 트랜스파일러는 기계 학습을 활용하여 회로 구조의 패턴과 하드웨어 제약 조건을 분석함으로써 최적의 트랜스파일링 전략을 예측합니다. 또한 강화 학습 기반의 합성 접근법을 활용해 파울리 네트워크 회로를 대상으로 하는 ‘패스(pass)’를 AIPauliNetworkSynthesis 적용할 수도 있습니다. 자세한 내용은 [2] 와 [3] 을 참조하십시오.
Rustiq 플러그인
Rustiq 플러그인은 트로터화 역학에서 흔히 사용되는 파울리 회전을 나타내는 연산에 PauliEvolutionGate 특화된 고급 합성 기법을 제공합니다. 이 방법은 해밀턴 연산 시뮬레이션 워크로드를 위해 깊이 낮은 회로 분해를 생성하도록 설계되었습니다. 자세한 내용은 [4]를 참조하십시오.
키 메트릭
우리는 다음 평가 지표를 바탕으로 세 가지 방법을 비교합니다:
- 2-큐비트 깊이 : 2-큐비트 게이트만을 집계한 회로의 깊이. 이는 실제 하드웨어에서 정확도를 저해하는 주요 요인이 되는 경우가 많습니다.
- 회로 크기(총 게이트 수) : 트랜스파일링된 회로의 총 게이트 수.
- 실행 시간 : 트랜스파일링에 소요된 실제 시간.
요구사항
이 튜토리얼을 시작하기 전에 다음이 설치되어 있는지 확인하세요:
- Qiskit SDK v2.0 또는 그 이후 버전이며, 시각화 기능을 지원합니다
- Qiskit Runtime v0.22 또는 그 이후 (
pip install qiskit-ibm-runtime) - Qiskit Aer (
pip install qiskit-aer) - 키스킷 IBM 트랜스파일러 (
pip install qiskit-ibm-transpiler) - 키스킷 AI 트랜스파일러 로컬 모드 (
pip install qiskit_ibm_ai_local_transpiler) - Networkx (
pip install networkx)
설정
from qiskit.circuit import QuantumCircuit
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2
from qiskit.circuit.library import PauliEvolutionGate
from qiskit_ibm_transpiler import generate_ai_pass_manager
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel, depolarizing_error
from collections import Counter
from statistics import mean, stdev
from scipy.sparse import SparseEfficiencyWarning
import time
import warnings
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import numpy as np
import json
import requests
import logging
# Suppress noisy loggers and warnings
logging.getLogger(
"qiskit_ibm_transpiler.wrappers.ai_local_synthesis"
).setLevel(logging.ERROR)
warnings.filterwarnings("ignore", category=FutureWarning)
warnings.filterwarnings("ignore", category=SparseEfficiencyWarning)
seed = 42 # Seed for reproducibility백엔드에 연결하기
소규모 예제와 대규모 예제 모두에 사용할 백엔드를 선택하십시오. 백엔드는 트랜스파일러가 대상으로 삼는 결합 맵과 기본 게이트를 결정합니다.
# QiskitRuntimeService.save_account(channel="ibm_quantum_platform",
# token="<YOUR-API-KEY>", overwrite=True, set_as_default=True)
service = QiskitRuntimeService(channel="ibm_quantum_platform")
backend = service.least_busy(operational=True, simulator=False)
print(f"Using backend: {backend.name}")Output:
Using backend: ibm_pittsburgh
패스 관리자 정의
세 가지 컴파일 방법을 설정하십시오.
# SABRE pass manager (Qiskit default at optimization level 3)
pm_sabre = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=seed
)# AI transpiler pass manager (local mode)
pm_ai = generate_ai_pass_manager(
backend=backend, optimization_level=3, ai_optimization_level=3
)Output:
Fetching 127 files: 0%| | 0/127 [00:00<?, ?it/s]
# Rustiq pass manager for PauliEvolutionGate synthesis
hls_config = HLSConfig(
PauliEvolution=[
(
"rustiq",
{
"nshuffles": 400,
"upto_phase": True,
"fix_clifford": True,
"preserve_order": False,
"metric": "depth",
},
)
]
)
pm_rustiq = generate_preset_pass_manager(
optimization_level=3,
backend=backend,
hls_config=hls_config,
seed_transpiler=seed,
)헬퍼 함수 정의하기
다음 함수는 주어진 패스 매니저를 사용하여 회로 목록을 트랜스파일하고, 각 회로에 대한 주요 지표(2-큐비트 깊이, 회로 크기, 실행 시간)를 기록합니다.
def capture_transpilation_metrics(
results, pass_manager, circuits, method_name
):
"""
Transpile circuits and append one metrics record per circuit to
``results``.
Args:
results (list): List of dicts to append the metrics records to.
pass_manager: Pass manager used for transpilation.
circuits (list): List of quantum circuits to transpile.
method_name (str): Name of the transpilation method.
Returns:
list: List of transpiled circuits.
"""
transpiled_circuits = []
for i, qc in enumerate(circuits):
start_time = time.time()
transpiled_qc = pass_manager.run(qc)
end_time = time.time()
# Decompose swaps for consistency across methods
transpiled_qc = transpiled_qc.decompose(gates_to_decompose=["swap"])
transpilation_time = end_time - start_time
two_qubit_depth = transpiled_qc.depth(
lambda x: x.operation.num_qubits == 2
)
circuit_size = transpiled_qc.size()
results.append(
{
"method": method_name,
"qc_name": qc.name,
"qc_index": i,
"num_qubits": qc.num_qubits,
"two_qubit_depth": two_qubit_depth,
"size": circuit_size,
"runtime": transpilation_time,
}
)
transpiled_circuits.append(transpiled_qc)
print(
f"[{method_name}] Circuit {i} ({qc.name}): "
f"2Q depth={two_qubit_depth}, size={circuit_size}, "
f"time={transpilation_time:.2f}s"
)
return transpiled_circuitsdef _method_order(results):
"""Return the distinct method names in their first-seen order."""
order = []
for r in results:
if r["method"] not in order:
order.append(r["method"])
return order
def print_summary_table(results):
"""
Print the mean and standard deviation of each metric per compilation
method, followed by the mean percent improvement relative to SABRE.
"""
metrics = [
("two_qubit_depth", "2Q Depth"),
("size", "Gate Count"),
("runtime", "Runtime (s)"),
]
methods = _method_order(results)
by_method = {m: [r for r in results if r["method"] == m] for m in methods}
sabre_by_index = {r["qc_index"]: r for r in by_method.get("SABRE", [])}
col_w = 22
name_w = max(len(m) for m in methods)
header = f"{'Method':<{name_w}}" + "".join(
f" {label:>{col_w}}" for _, label in metrics
)
print("Mean +/- std per compilation method")
print(header)
print("-" * len(header))
for method in methods:
cells = []
for key, _ in metrics:
values = [r[key] for r in by_method[method]]
std = stdev(values) if len(values) > 1 else 0.0
cells.append(f"{mean(values):,.1f} +/- {std:,.1f}")
print(
f"{method:<{name_w}}" + "".join(f" {c:>{col_w}}" for c in cells)
)
others = [m for m in methods if m != "SABRE"]
if others and sabre_by_index:
print()
print("Mean % improvement vs SABRE (positive = better than SABRE)")
print(header)
print("-" * len(header))
for method in others:
cells = []
for key, _ in metrics:
pct = [
(sabre_by_index[r["qc_index"]][key] - r[key])
/ sabre_by_index[r["qc_index"]][key]
* 100
for r in by_method[method]
if sabre_by_index.get(r["qc_index"])
and sabre_by_index[r["qc_index"]][key]
]
if pct:
std = stdev(pct) if len(pct) > 1 else 0.0
cells.append(f"{mean(pct):+.1f}% +/- {std:.1f}%")
else:
cells.append("n/a")
print(
f"{method:<{name_w}}"
+ "".join(f" {c:>{col_w}}" for c in cells)
)def print_per_circuit_comparison(results, num_rows=5):
"""
Print a per-metric comparison of the compilation methods for the
first ``num_rows`` circuits (sorted by qubit count). The best
(lowest) value for each metric is marked with an asterisk.
"""
metrics = [
("two_qubit_depth", "2Q Depth"),
("size", "Gate Count"),
("runtime", "Runtime (s)"),
]
methods = _method_order(results)
by_index = {}
for r in results:
by_index.setdefault(r["qc_index"], {})[r["method"]] = r
ordered = sorted(
by_index.items(),
key=lambda kv: (next(iter(kv[1].values()))["num_qubits"], kv[0]),
)[:num_rows]
for key, label in metrics:
print(f"{label} (first {num_rows} circuits by qubit count); * = best")
header = f"{'Idx':>3} {'Circuit':<16} {'Q':>3}" + "".join(
f"{m:>9}" for m in methods
)
print(header)
print("-" * len(header))
for idx, method_map in ordered:
any_record = next(iter(method_map.values()))
present = {
m: method_map[m][key] for m in methods if m in method_map
}
best = min(present.values())
line = (
f"{idx:>3} {any_record['qc_name'][:16]:<16} "
f"{any_record['num_qubits']:>3}"
)
for m in methods:
value = method_map[m][key]
text = f"{value:.2f}" if key == "runtime" else f"{int(value)}"
if value == best:
text += "*"
line += f"{text:>9}"
print(line)
print()Hamlib에서 해밀토니안 회로 불러오기
Benchpress 저장소에서 대표적인 해밀토니안 집합을 불러와 회로를 구성합니다 PauliEvolutionGate . 백엔드의 큐비트 수를 초과하는 회로와, 분해된 크기가 1,500 게이트를 초과하는 회로는 (트랜스필레이션 시간을 적정 수준으로 유지하기 위해) 제거됩니다.
# Obtain the Hamiltonian JSON from the benchpress repository
url = (
"https://raw.githubusercontent.com/Qiskit/benchpress/"
"e7b29ef7be4cc0d70237b8fdc03edbd698908eff/"
"benchpress/hamiltonian/hamlib/100_representative.json"
)
response = requests.get(url)
response.raise_for_status()
ham_records = json.loads(response.text)
# Remove circuits that are too large for the backend
ham_records = [
h for h in ham_records if h["ham_qubits"] <= backend.num_qubits
]
# Build PauliEvolutionGate circuits
qc_ham_list = []
for h in ham_records:
terms = h["ham_hamlib_hamiltonian_terms"]
coeff = h["ham_hamlib_hamiltonian_coefficients"]
num_qubits = h["ham_qubits"]
name = h["ham_problem"]
evo_gate = PauliEvolutionGate(SparsePauliOp(terms, coeff))
qc = QuantumCircuit(num_qubits)
qc.name = name
qc.append(evo_gate, range(num_qubits))
qc_ham_list.append(qc)
# Remove circuits whose decomposed size exceeds 1500 gates so that
# transpilation completes in a reasonable time frame
qc_ham_list = [qc for qc in qc_ham_list if qc.decompose().size() <= 1500]
print(f"Total Hamiltonian circuits loaded: {len(qc_ham_list)}")
min_qubits = min(qc.num_qubits for qc in qc_ham_list)
max_qubits = max(qc.num_qubits for qc in qc_ham_list)
print(f"Qubit range: {min_qubits} to {max_qubits}")Output:
Total Hamiltonian circuits loaded: 42
Qubit range: 2 to 112
회로를 소규모(20 큐비트 미만) 그룹과 대규모(20 큐비트 이상) 그룹으로 나눕니다.
qc_small = [qc for qc in qc_ham_list if qc.num_qubits < 20]
qc_large = [qc for qc in qc_ham_list if qc.num_qubits >= 20]
print(f"Small-scale circuits (<20 qubits): {len(qc_small)}")
print(f"Large-scale circuits (>=20 qubits): {len(qc_large)}")Output:
Small-scale circuits (<20 qubits): 20
Large-scale circuits (>=20 qubits): 22
변환 처리 전에 소규모 해밀턴 회로 중 하나를 미리 살펴보세요.
# We decompose the circuit here, otherwise it would just be a PauliEvolutionGate box,
# which isn't very informative to look at!
qc_small[0].decompose().draw("mpl", fold=-1)Output:
소규모 예시
이 절에서는 20 큐비트 미만의 해밀토니안 회로를 대상으로 세 가지 컴파일링 방법에 대한 벤치마크를 수행한다. 이 회로들은 빠르게 트랜스파일되며, 각 메서드가 중간 정도의 복잡성을 가진 회로를 어떻게 처리하는지 명확하게 보여줍니다.
1단계: 고전적 입력을 양자 문제에 매핑하기
각 해밀토니안은 회로로 PauliEvolutionGate 인코딩됩니다. 이 회로들은 Hamlib 벤치마크 데이터를 바탕으로 설정 섹션에서 이미 구축되어 있었습니다.
2단계: 양자 하드웨어 실행을 위한 문제 최적화
우리는 세 가지 패스 관리자를 각각 사용하여 모든 소규모 회로를 트랜스파일한 다음, 메트릭을 수집합니다.
results_small = []
tqc_sabre_small = capture_transpilation_metrics(
results_small, pm_sabre, qc_small, "SABRE"
)
tqc_ai_small = capture_transpilation_metrics(
results_small, pm_ai, qc_small, "AI"
)
tqc_rustiq_small = capture_transpilation_metrics(
results_small, pm_rustiq, qc_small, "Rustiq"
)Output:
[SABRE] Circuit 0 (all-vib-bh): 2Q depth=3, size=30, time=2.09s
[SABRE] Circuit 1 (all-vib-c2h): 2Q depth=18, size=111, time=0.01s
[SABRE] Circuit 2 (all-vib-o3): 2Q depth=6, size=58, time=0.00s
[SABRE] Circuit 3 (all-vib-c2h): 2Q depth=2, size=37, time=0.01s
[SABRE] Circuit 4 (graph-gnp_k-2): 2Q depth=24, size=126, time=0.01s
[SABRE] Circuit 5 (LiH): 2Q depth=66, size=285, time=0.01s
[SABRE] Circuit 6 (all-vib-fccf): 2Q depth=66, size=339, time=0.01s
[SABRE] Circuit 7 (all-vib-ch2): 2Q depth=88, size=413, time=0.01s
[SABRE] Circuit 8 (all-vib-f2): 2Q depth=180, size=1000, time=0.02s
[SABRE] Circuit 9 (all-vib-bhf2): 2Q depth=18, size=223, time=0.03s
[SABRE] Circuit 10 (graph-gnp_k-4): 2Q depth=122, size=675, time=0.02s
[SABRE] Circuit 11 (Be2): 2Q depth=343, size=1628, time=0.03s
[SABRE] Circuit 12 (all-vib-fccf): 2Q depth=14, size=134, time=0.00s
[SABRE] Circuit 13 (uf20-ham): 2Q depth=50, size=341, time=0.01s
[SABRE] Circuit 14 (TSP_Ncity-4): 2Q depth=118, size=615, time=0.01s
[SABRE] Circuit 15 (graph-complete_bipart): 2Q depth=232, size=1420, time=0.03s
[SABRE] Circuit 16 (all-vib-cyclo_propene): 2Q depth=18, size=354, time=0.93s
[SABRE] Circuit 17 (all-vib-hno): 2Q depth=6, size=174, time=0.14s
[SABRE] Circuit 18 (all-vib-fccf): 2Q depth=30, size=286, time=0.01s
[SABRE] Circuit 19 (tfim): 2Q depth=31, size=232, time=0.03s
[AI] Circuit 0 (all-vib-bh): 2Q depth=3, size=30, time=0.01s
Fetching 4 files: 0%| | 0/4 [00:00<?, ?it/s]
[AI] Circuit 1 (all-vib-c2h): 2Q depth=18, size=101, time=0.18s
[AI] Circuit 2 (all-vib-o3): 2Q depth=6, size=58, time=0.01s
[AI] Circuit 3 (all-vib-c2h): 2Q depth=2, size=37, time=0.01s
[AI] Circuit 4 (graph-gnp_k-2): 2Q depth=24, size=133, time=0.07s
[AI] Circuit 5 (LiH): 2Q depth=62, size=267, time=8.00s
[AI] Circuit 6 (all-vib-fccf): 2Q depth=65, size=300, time=0.18s
[AI] Circuit 7 (all-vib-ch2): 2Q depth=79, size=353, time=0.16s
[AI] Circuit 8 (all-vib-f2): 2Q depth=176, size=998, time=0.43s
[AI] Circuit 9 (all-vib-bhf2): 2Q depth=18, size=194, time=0.11s
[AI] Circuit 10 (graph-gnp_k-4): 2Q depth=114, size=668, time=0.18s
[AI] Circuit 11 (Be2): 2Q depth=292, size=1382, time=0.88s
[AI] Circuit 12 (all-vib-fccf): 2Q depth=14, size=134, time=0.01s
[AI] Circuit 13 (uf20-ham): 2Q depth=40, size=330, time=0.16s
[AI] Circuit 14 (TSP_Ncity-4): 2Q depth=96, size=600, time=0.29s
[AI] Circuit 15 (graph-complete_bipart): 2Q depth=231, size=1531, time=0.46s
[AI] Circuit 16 (all-vib-cyclo_propene): 2Q depth=18, size=309, time=0.25s
[AI] Circuit 17 (all-vib-hno): 2Q depth=10, size=198, time=0.15s
[AI] Circuit 18 (all-vib-fccf): 2Q depth=34, size=402, time=0.02s
[AI] Circuit 19 (tfim): 2Q depth=44, size=311, time=0.15s
[Rustiq] Circuit 0 (all-vib-bh): 2Q depth=3, size=30, time=0.01s
[Rustiq] Circuit 1 (all-vib-c2h): 2Q depth=13, size=69, time=0.00s
[Rustiq] Circuit 2 (all-vib-o3): 2Q depth=13, size=82, time=0.01s
[Rustiq] Circuit 3 (all-vib-c2h): 2Q depth=2, size=40, time=0.01s
[Rustiq] Circuit 4 (graph-gnp_k-2): 2Q depth=31, size=132, time=0.01s
[Rustiq] Circuit 5 (LiH): 2Q depth=59, size=285, time=0.01s
[Rustiq] Circuit 6 (all-vib-fccf): 2Q depth=34, size=193, time=0.00s
[Rustiq] Circuit 7 (all-vib-ch2): 2Q depth=49, size=302, time=0.01s
[Rustiq] Circuit 8 (all-vib-f2): 2Q depth=141, size=807, time=0.02s
[Rustiq] Circuit 9 (all-vib-bhf2): 2Q depth=13, size=146, time=0.02s
[Rustiq] Circuit 10 (graph-gnp_k-4): 2Q depth=129, size=683, time=0.02s
[Rustiq] Circuit 11 (Be2): 2Q depth=220, size=1101, time=0.02s
[Rustiq] Circuit 12 (all-vib-fccf): 2Q depth=53, size=333, time=0.01s
[Rustiq] Circuit 13 (uf20-ham): 2Q depth=63, size=425, time=0.01s
[Rustiq] Circuit 14 (TSP_Ncity-4): 2Q depth=123, size=767, time=0.02s
[Rustiq] Circuit 15 (graph-complete_bipart): 2Q depth=309, size=2107, time=0.05s
[Rustiq] Circuit 16 (all-vib-cyclo_propene): 2Q depth=16, size=283, time=0.32s
[Rustiq] Circuit 17 (all-vib-hno): 2Q depth=19, size=291, time=0.32s
[Rustiq] Circuit 18 (all-vib-fccf): 2Q depth=44, size=546, time=0.02s
[Rustiq] Circuit 19 (tfim): 2Q depth=24, size=416, time=0.01s
아래 표는 모든 소규모 회로에 대한 각 지표의 평균과 표준편차, 그리고 SABRE 대비 개선율을 요약한 것입니다. 회로 크기가 매우 다양하기 때문에, 표준편차는 평균값을 해석하는 데 중요한 맥락을 제공합니다.
print_summary_table(results_small)Output:
Mean +/- std per compilation method
Method 2Q Depth Gate Count Runtime (s)
------------------------------------------------------------------------------
SABRE 71.8 +/- 89.6 424.1 +/- 446.0 0.2 +/- 0.5
AI 67.3 +/- 80.2 416.8 +/- 426.7 0.6 +/- 1.8
Rustiq 67.9 +/- 80.0 451.9 +/- 484.7 0.0 +/- 0.1
Mean % improvement vs SABRE (positive = better than SABRE)
Method 2Q Depth Gate Count Runtime (s)
------------------------------------------------------------------------------
AI -2.1% +/- 19.8% -0.6% +/- 14.7% -5635.1% +/- 20725.2%
Rustiq -25.3% +/- 85.4% -16.3% +/- 50.4% -7.0% +/- 60.6%
회로별 표는 개별 회로에서 각 방식의 성능을 비교한 결과를 보여줍니다. 각 지표별 최고 수치는 별표로 표시되어 있습니다. 가장 간단한 회로의 경우, 세 가지 방법 모두 종종 동일한 결과로 수렴한다는 점에 유의하십시오.
print_per_circuit_comparison(results_small, num_rows=8)Output:
2Q Depth (first 8 circuits by qubit count); * = best
Idx Circuit Q SABRE AI Rustiq
----------------------------------------------------
0 all-vib-bh 2 3* 3* 3*
1 all-vib-c2h 3 18 18 13*
2 all-vib-o3 4 6* 6* 13
3 all-vib-c2h 4 2* 2* 2*
4 graph-gnp_k-2 4 24* 24* 31
5 LiH 4 66 62 59*
6 all-vib-fccf 4 66 65 34*
7 all-vib-ch2 4 88 79 49*
Gate Count (first 8 circuits by qubit count); * = best
Idx Circuit Q SABRE AI Rustiq
----------------------------------------------------
0 all-vib-bh 2 30* 30* 30*
1 all-vib-c2h 3 111 101 69*
2 all-vib-o3 4 58* 58* 82
3 all-vib-c2h 4 37* 37* 40
4 graph-gnp_k-2 4 126* 133 132
5 LiH 4 285 267* 285
6 all-vib-fccf 4 339 300 193*
7 all-vib-ch2 4 413 353 302*
Runtime (s) (first 8 circuits by qubit count); * = best
Idx Circuit Q SABRE AI Rustiq
----------------------------------------------------
0 all-vib-bh 2 2.09 0.01 0.01*
1 all-vib-c2h 3 0.01 0.18 0.00*
2 all-vib-o3 4 0.00* 0.01 0.01
3 all-vib-c2h 4 0.01 0.01 0.01*
4 graph-gnp_k-2 4 0.01* 0.07 0.01
5 LiH 4 0.01* 8.00 0.01
6 all-vib-fccf 4 0.01 0.18 0.00*
7 all-vib-ch2 4 0.01 0.16 0.01*
결과 시각화
아래 그래프는 회로별로 각 평가 지표를 기준으로 세 가지 방법을 비교한 것입니다. 여러 회로가 동일한 큐비트 수를 가질 수 있으므로, 회로는 큐비트 수에 따라 정렬되며 x축의 인덱스로 표시됩니다.
def plot_transpilation_comparison(results, title_prefix):
"""
Create a three-panel figure comparing compilation methods on
two-qubit depth, circuit size, and runtime.
Circuits are sorted by qubit count and plotted by circuit index.
"""
methods = _method_order(results)
palette = {"SABRE": "#1f77b4", "AI": "#ff7f0e", "Rustiq": "#2ca02c"}
markers = {"SABRE": "o", "AI": "^", "Rustiq": "s"}
# Order circuits by qubit count (then index) and map to plot positions
ref = sorted(
[r for r in results if r["method"] == methods[0]],
key=lambda r: (r["num_qubits"], r["qc_index"]),
)
pos_map = {r["qc_index"]: pos for pos, r in enumerate(ref)}
tick_positions = [pos_map[r["qc_index"]] for r in ref]
tick_labels = [
f"{pos_map[r['qc_index']]} ({r['num_qubits']}q)" for r in ref
]
metrics = [
("two_qubit_depth", "Two-Qubit Depth"),
("size", "Total Gate Count (Circuit Size)"),
("runtime", "Transpilation Runtime (s)"),
]
fig, axes = plt.subplots(1, 3, figsize=(20, 5.5))
fig.suptitle(title_prefix, fontsize=15, fontweight="bold", y=1.02)
for ax, (metric, ylabel) in zip(axes, metrics):
for method in methods:
subset = sorted(
[r for r in results if r["method"] == method],
key=lambda r: pos_map[r["qc_index"]],
)
ax.plot(
[pos_map[r["qc_index"]] for r in subset],
[r[metric] for r in subset],
marker=markers.get(method, "o"),
label=method,
color=palette.get(method, None),
linewidth=1.5,
markersize=6,
alpha=0.85,
)
ax.set_xlabel("Circuit Index (num qubits)", fontsize=11)
ax.set_ylabel(ylabel, fontsize=11)
ax.legend(frameon=True, fontsize=9)
ax.grid(True, linestyle="--", alpha=0.4)
step = max(1, len(tick_positions) // 15)
ax.set_xticks(tick_positions[::step])
ax.set_xticklabels(
[tick_labels[i] for i in range(0, len(tick_labels), step)],
fontsize=7,
rotation=45,
ha="right",
)
plt.tight_layout()
plt.show()def plot_pct_improvement_vs_sabre(results, title_prefix):
"""
Plot the per-circuit percent improvement of each non-SABRE method
relative to SABRE, for each metric. A positive value means the
method improved on SABRE; negative means SABRE was better.
"""
metrics = [
("two_qubit_depth", "2Q Depth"),
("size", "Gate Count"),
("runtime", "Runtime"),
]
palette = {"AI": "#ff7f0e", "Rustiq": "#2ca02c"}
markers = {"AI": "^", "Rustiq": "s"}
methods = _method_order(results)
sabre = sorted(
[r for r in results if r["method"] == "SABRE"],
key=lambda r: (r["num_qubits"], r["qc_index"]),
)
other_methods = [m for m in methods if m != "SABRE"]
tick_positions = list(range(len(sabre)))
tick_labels = [
f"{i} ({sabre[i]['num_qubits']}q)" for i in range(len(sabre))
]
fig, axes = plt.subplots(1, 3, figsize=(20, 5.5))
fig.suptitle(
f"{title_prefix}: % Improvement over SABRE",
fontsize=15,
fontweight="bold",
y=1.02,
)
for ax, (metric, label) in zip(axes, metrics):
ax.axhline(0, color="#1f77b4", linewidth=2, label="SABRE (baseline)")
for method in other_methods:
data = sorted(
[r for r in results if r["method"] == method],
key=lambda r: (r["num_qubits"], r["qc_index"]),
)
pct = [
(sabre[i][metric] - data[i][metric]) / sabre[i][metric] * 100
for i in range(len(sabre))
]
ax.plot(
tick_positions,
pct,
marker=markers.get(method, "o"),
label=method,
color=palette.get(method, None),
linewidth=1.5,
markersize=6,
alpha=0.85,
)
ax.set_xlabel("Circuit Index (num qubits)", fontsize=11)
ax.set_ylabel(f"% Improvement ({label})", fontsize=11)
ax.legend(frameon=True, fontsize=9)
ax.grid(True, linestyle="--", alpha=0.4)
step = max(1, len(tick_positions) // 15)
ax.set_xticks(tick_positions[::step])
ax.set_xticklabels(
[tick_labels[i] for i in range(0, len(tick_labels), step)],
fontsize=7,
rotation=45,
ha="right",
)
ylims = ax.get_ylim()
ax.axhspan(0, max(ylims[1], 1), alpha=0.04, color="green")
ax.axhspan(min(ylims[0], -1), 0, alpha=0.04, color="red")
plt.tight_layout()
plt.show()plot_transpilation_comparison(
results_small,
"Small-Scale Hamiltonian Circuits: Compilation Comparison",
)Output:
plot_pct_improvement_vs_sabre(
results_small,
"Small-Scale Hamiltonian Circuits",
)Output:
이 규모에서는 세 명의 패스 매니저 모두 좋은 성과를 보였으며, 그들의 평균 성적은 서로 비슷합니다. 이는 주로 회로 규모가 작을수록 추가적인 최적화를 위한 여지가 제한적이기 때문에, 해당 방법들이 유사한 해로 수렴하는 경향이 있기 때문이다.
이 예시에서 Rustiq는 가장 큰 변동성을 보이며, 2-큐비트 깊이와 게이트 수 모두에서 가장 큰 이상치를 기록합니다. 이러한 변동성 때문에 때로는 뒤처지기도 하지만, 동시에 Rustiq가 다른 두 방법보다 더 나은 해결책을 찾아내기도 한다는 뜻이기도 합니다. AI 트랜스파일러는 SABRE 및 Rustiq에 비해 결과의 안정성이 더 뛰어나며, 대부분의 회로에서 큰 편차 없이 유사한 결과를 보여줍니다.
실행 속도 면에서 SABRE와 Rustiq는 모두 빠르지만, AI 기반 트랜스파일러는 특정 회로에서 눈에 띄게 느립니다.
지표별 최우수 성과 방법
아래 차트는 각 지표별로 각 방법이 최상의(가장 낮은) 값을 달성한 빈도를 보여줍니다. 동등한 결과가 나올 수 있습니다: 더 단순한 회로의 경우, 여러 가지 방법이 동일한 최적의 2-큐비트 깊이 또는 게이트 수에 도달할 수 있습니다. 동점이 발생할 경우, 동점을 기록한 모든 메서드가 점수를 받기 때문에 특정 지표의 백분율 합계가 100%를 초과할 수 있습니다.
def plot_best_method_bars(results, metrics_list=None):
"""
Plot a grouped bar chart showing the percentage of circuits
where each method achieved the best (lowest) value for each metric.
Ties are counted for all tied methods, so percentages per metric
can sum to more than 100%.
"""
if metrics_list is None:
metrics_list = ["two_qubit_depth", "size", "runtime"]
labels = {
"two_qubit_depth": "2Q Depth",
"size": "Gate Count",
"runtime": "Runtime",
}
methods = _method_order(results)
palette = {"SABRE": "#1f77b4", "AI": "#ff7f0e", "Rustiq": "#2ca02c"}
by_index = {}
for r in results:
by_index.setdefault(r["qc_index"], []).append(r)
n_circuits = len(by_index)
win_data = {m: [] for m in methods}
tie_counts = []
metric_labels = []
for metric in metrics_list:
metric_labels.append(
labels.get(metric, metric.replace("_", " ").title())
)
counts = Counter()
ties = 0
for group in by_index.values():
min_val = min(r[metric] for r in group)
best = [r["method"] for r in group if r[metric] == min_val]
if len(best) > 1:
ties += 1
counts.update(best)
tie_counts.append(ties)
for m in methods:
win_data[m].append(counts.get(m, 0) / n_circuits * 100)
x = np.arange(len(metric_labels))
width = 0.22
fig, ax = plt.subplots(figsize=(8, 5))
for i, method in enumerate(methods):
bars = ax.bar(
x + i * width,
win_data[method],
width,
label=method,
color=palette.get(method, None),
edgecolor="black",
linewidth=0.5,
)
for bar in bars:
height = bar.get_height()
if height > 0:
ax.text(
bar.get_x() + bar.get_width() / 2,
height + 1.5,
f"{height:.0f}%",
ha="center",
va="bottom",
fontsize=9,
)
# Annotate tie counts below each metric label
for j, ties in enumerate(tie_counts):
if ties > 0:
ax.text(
x[j] + width,
-8,
f"({ties} tie{'s' if ties != 1 else ''})",
ha="center",
va="top",
fontsize=8,
color="gray",
)
ax.set_xticks(x + width)
ax.set_xticklabels(metric_labels, fontsize=11)
ax.set_ylabel("Circuits with best value (%)", fontsize=11)
ax.set_title(
"Best-Performing Method by Metric (ties counted for all tied methods)",
fontsize=12,
fontweight="bold",
)
ax.legend(frameon=True, fontsize=10)
ax.set_ylim(-12, 120)
ax.yaxis.set_major_formatter(ticker.PercentFormatter())
ax.grid(axis="y", linestyle="--", alpha=0.4)
plt.tight_layout()
plt.show()plot_best_method_bars(results_small)Output:
이 예시에서, 세 가지 방법은 소규모 회로에서 매우 유사한 성능을 보입니다. 2-큐비트 회로의 깊이와 게이트 수를 기준으로 볼 때, 각 방법이 최상의 성능을 보이는 회로의 비율은 비슷하며(대략 35~55%), 가장 단순한 회로의 경우 여러 방법이 동일한 최적 해를 찾아내는 경우가 많아 많은 회로에서 동률을 기록한다. 가장 뚜렷한 차이점은 실행 시간입니다. SABRE와 Rustiq는 각각 약 절반 정도의 회로에서 가장 빠른 속도를 보이는 반면, AI 기반 트랜스파일러가 가장 빠른 경우는 거의 없습니다. 세 가지 지표를 종합적으로 고려할 때, Rustiq가 전반적으로 약간 우위를 점하고 있다. 2-큐비트 깊이 부문에서는 가장 높은 승률을 기록하고 있으며, 게이트 수와 실행 시간 부문에서도 경쟁력을 유지하고 있다.
3단계: Qiskit primitives 명령어로 실행합니다
트랜스필레이션 품질이 노이즈 조건 하에서 실행에 어떤 영향을 미치는지 평가하기 위해, 미러 회로 기법을 사용합니다. 트랜스파일된 각 회로 에 대해, 그 역회로 를 뒤에 덧붙이면, 결합된 회로 는 이론적으로 항등 회로가 된다. 상태에서 시작할 때, 완벽한(잡음이 없는) 실행은 확률 1로 모두 0인 비트열을 반환할 것이다.
실제로는 게이트 오류가 회로 전반에 걸쳐 누적되기 때문에, 를 복구할 확률이 떨어집니다. 게이트 수가 적고 회로 깊이가 얕은 회로를 생성하는 합성 방식은 노이즈가 덜 발생합니다.
미러 회로 방식은 예상 출력이 항상 이므로 매우 간단할 뿐만 아니라 어떤 규모의 회로에도 적용할 수 있으며, 이상적인 상태에 대한 고전적 시뮬레이션이 필요하지 않습니다. 다만, 다음 사항에 유의해야 합니다. 미러 회로는 실제 회로를 대리하는 것일 뿐(실제 회로 그 자체가 아님)이며, 게이트 수를 두 배로 늘려(이로 인해 노이즈의 영향이 과장됨) 미러 경계를 가로질러 노이즈가 대칭적으로 상쇄될 경우 특정 오류를 과소평가할 수 있습니다.
소규모 집합에서 회로 인덱스 6을 선택하고, 간단한 탈편광 잡음 모델을 적용한 Aer 시뮬레이터에서 미러 회로를 실행합니다.
# Select circuit index 6 from the small-scale transpiled circuits
test_idx = 6
test_circuit = qc_small[test_idx]
print(f"Test circuit: {test_circuit.name}, {test_circuit.num_qubits} qubits")
# Get the transpiled versions
tqc_methods_small = {
"SABRE": tqc_sabre_small[test_idx],
"AI": tqc_ai_small[test_idx],
"Rustiq": tqc_rustiq_small[test_idx],
}
# Show transpilation metrics for this circuit
print(f"\nTranspilation metrics for circuit index {test_idx}:")
for method, tqc in tqc_methods_small.items():
depth_2q = tqc.depth(lambda x: x.operation.num_qubits == 2)
size = tqc.size()
print(f" {method:8s} 2Q depth={depth_2q:5d} size={size:6d}")Output:
Test circuit: all-vib-fccf, 4 qubits
Transpilation metrics for circuit index 6:
SABRE 2Q depth= 66 size= 339
AI 2Q depth= 65 size= 300
Rustiq 2Q depth= 34 size= 193
미러 회로를 구축하고( 를 추가), 시뮬레이터가 활성 큐비트만 처리하도록 연속적인 큐비트 인덱스로 재매핑한 다음, 노이즈가 있는 Aer 시뮬레이터에서 실행합니다.
def remap_to_contiguous(tqc):
"""Remap a transpiled circuit to use contiguous qubit indices.
Transpiled circuits target specific physical qubits (e.g., qubit 45, 67)
on a large backend. This remaps them to 0, 1, 2, ... so Aer only
simulates the active qubits.
"""
active = sorted(
{tqc.find_bit(q).index for inst in tqc.data for q in inst.qubits}
)
qubit_map = {old: new for new, old in enumerate(active)}
new_qc = QuantumCircuit(len(active))
for inst in tqc.data:
old_indices = [tqc.find_bit(q).index for q in inst.qubits]
new_qc.append(inst.operation, [qubit_map[i] for i in old_indices])
return new_qc
def build_mirror_circuit(tqc):
"""Build a mirror circuit: U followed by U-dagger, with measurements.
The combined circuit U-dagger @ U should be the identity, so measuring
all zeros indicates a noise-free execution.
"""
tqc_compact = remap_to_contiguous(tqc)
mirror = tqc_compact.compose(tqc_compact.inverse())
mirror.measure_all()
return mirror
# Build a simple depolarizing noise model
noise_model = NoiseModel()
noise_model.add_all_qubit_quantum_error(
depolarizing_error(0.001, 1),
["sx", "x", "rz"], # ~0.1% per 1Q gate
)
noise_model.add_all_qubit_quantum_error(
depolarizing_error(0.01, 2),
["cx", "ecr"], # ~1% per 2Q gate
)
aer_sim = AerSimulator(noise_model=noise_model)
shots = 10000
fidelities = {}
for method, tqc in tqc_methods_small.items():
mirror = build_mirror_circuit(tqc)
sampler = SamplerV2(mode=aer_sim)
job = sampler.run([mirror], shots=shots)
result = job.result()
counts = result[0].data.meas.get_counts()
# Fidelity = fraction of all-zeros (error-free) outcomes
n_qubits = mirror.num_qubits - mirror.num_clbits # active qubits
all_zeros = "0" * mirror.num_qubits
fidelity = counts.get(all_zeros, 0) / shots
fidelities[method] = fidelity
print(
f"{method:8s} P(|00...0>) = {fidelity:.4f} "
f"({counts.get(all_zeros, 0)}/{shots})"
)Output:
SABRE P(|00...0>) = 0.7796 (7796/10000)
AI P(|00...0>) = 0.8073 (8073/10000)
Rustiq P(|00...0>) = 0.8923 (8923/10000)
def plot_mirror_results(tqc_methods, fidelities, circuit_name):
"""
Plot a three-panel comparison: fidelity, 2Q depth,
and gate count for each compilation method.
"""
methods = list(tqc_methods.keys())
palette = {"SABRE": "#1f77b4", "AI": "#ff7f0e", "Rustiq": "#2ca02c"}
colors = [palette.get(m, "gray") for m in methods]
fidelity_vals = [fidelities[m] for m in methods]
depth_vals = [
tqc_methods[m].depth(lambda x: x.operation.num_qubits == 2)
for m in methods
]
size_vals = [tqc_methods[m].size() for m in methods]
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
fig.suptitle(
f"Mirror Circuit Results: {circuit_name}",
fontsize=14,
fontweight="bold",
y=1.02,
)
def _annotate_bars(ax, bars, values, fmt="{}"):
ymax = ax.get_ylim()[1]
for bar, val in zip(bars, values):
label = fmt.format(val)
y = val + ymax * 0.03
ax.text(
bar.get_x() + bar.get_width() / 2,
y,
label,
ha="center",
va="bottom",
fontsize=10,
fontweight="bold",
)
# Panel 1: Survival Probability
bars = axes[0].bar(
methods, fidelity_vals, color=colors, edgecolor="black", linewidth=0.5
)
axes[0].set_ylabel("Fidelity P(|00...0>)", fontsize=11)
axes[0].set_title("Fidelity (higher is better)", fontsize=12)
axes[0].set_ylim(
0, max(fidelity_vals) * 1.18 if max(fidelity_vals) > 0 else 1.0
)
axes[0].grid(axis="y", linestyle="--", alpha=0.4)
_annotate_bars(axes[0], bars, fidelity_vals, fmt="{:.4f}")
# Panel 2: Two-Qubit Depth
bars = axes[1].bar(
methods, depth_vals, color=colors, edgecolor="black", linewidth=0.5
)
axes[1].set_ylabel("Two-Qubit Depth", fontsize=11)
axes[1].set_title("2Q Depth (lower is better)", fontsize=12)
axes[1].set_ylim(0, max(depth_vals) * 1.18)
axes[1].grid(axis="y", linestyle="--", alpha=0.4)
_annotate_bars(axes[1], bars, depth_vals)
# Panel 3: Gate Count
bars = axes[2].bar(
methods, size_vals, color=colors, edgecolor="black", linewidth=0.5
)
axes[2].set_ylabel("Total Gate Count", fontsize=11)
axes[2].set_title("Gate Count (lower is better)", fontsize=12)
axes[2].set_ylim(0, max(size_vals) * 1.18)
axes[2].grid(axis="y", linestyle="--", alpha=0.4)
_annotate_bars(axes[2], bars, size_vals)
plt.tight_layout()
plt.show()
plot_mirror_results(tqc_methods_small, fidelities, test_circuit.name)Output:
관측값
2-큐비트 깊이가 가장 낮고 게이트 수가 가장 적은 방법이 가장 높은 충실도를 달성하는데, 이는 회로 길이가 짧을수록 노이즈가 덜 축적된다는 예상과 일치한다. 탈분극 잡음 모델 하에서는 깊이와 게이트 수의 미세한 차이조차도 재현도에 측정 가능한 차이로 나타납니다.
이 결과는 단일 회로에 대한 것임을 유의하시기 바랍니다. 해밀토니안 구조에 따라 회로마다 각 방법의 상대적 순위가 달라질 수 있다.
대규모 하드웨어 예시
이 절에서는 20개 이상의 큐비트로 구성된 해밀토니안 회로를 대상으로, 앞서 소개한 세 가지 컴파일링 방법에 대한 벤치마크를 수행합니다. 이러한 회로들은 실제 해밀턴 연산 시뮬레이션 워크로드를 더 잘 반영하며, 각 방법이 회로 품질 및 컴파일 시간 측면에서 어떻게 확장성을 보이는지 테스트합니다.
1~4단계 통합
이 워크플로는 소규모 예제와 동일한 구조를 따릅니다. 우리는 각 방법을 사용하여 모든 대규모 회로를 트랜스파일하고, 메트릭을 수집한 뒤, 미러 회로를 실제 양자 하드웨어에 제출합니다.
results_large = []
tqc_sabre_large = capture_transpilation_metrics(
results_large, pm_sabre, qc_large, "SABRE"
)
tqc_ai_large = capture_transpilation_metrics(
results_large, pm_ai, qc_large, "AI"
)
tqc_rustiq_large = capture_transpilation_metrics(
results_large, pm_rustiq, qc_large, "Rustiq"
)Output:
[SABRE] Circuit 0 (all-vib-hc3h2cn): 2Q depth=2, size=258, time=0.16s
[SABRE] Circuit 1 (ham-graph-gnp_k-5): 2Q depth=345, size=4036, time=0.08s
[SABRE] Circuit 2 (TSP_Ncity-5): 2Q depth=187, size=2045, time=0.04s
[SABRE] Circuit 3 (tfim): 2Q depth=100, size=489, time=0.21s
[SABRE] Circuit 4 (all-vib-h2co): 2Q depth=30, size=570, time=0.18s
[SABRE] Circuit 5 (uuf100-ham): 2Q depth=414, size=4779, time=0.09s
[SABRE] Circuit 6 (uuf100-ham): 2Q depth=523, size=5667, time=0.11s
[SABRE] Circuit 7 (graph-gnp_k-4): 2Q depth=3028, size=24885, time=0.39s
[SABRE] Circuit 8 (uf100-ham): 2Q depth=700, size=8271, time=0.15s
[SABRE] Circuit 9 (uf100-ham): 2Q depth=698, size=8957, time=0.15s
[SABRE] Circuit 10 (TSP_Ncity-7): 2Q depth=432, size=6353, time=0.12s
[SABRE] Circuit 11 (all-vib-cyclo_propene): 2Q depth=30, size=1144, time=0.20s
[SABRE] Circuit 12 (TSP_Ncity-8): 2Q depth=704, size=10287, time=0.18s
[SABRE] Circuit 13 (uf100-ham): 2Q depth=2454, size=30195, time=0.46s
[SABRE] Circuit 14 (tfim): 2Q depth=245, size=3670, time=0.08s
[SABRE] Circuit 15 (flat100-ham): 2Q depth=154, size=3836, time=0.12s
[SABRE] Circuit 16 (graph-regular_reg-4): 2Q depth=863, size=14063, time=0.22s
[SABRE] Circuit 17 (tfim): 2Q depth=581, size=8810, time=0.15s
[SABRE] Circuit 18 (FH_D-1): 2Q depth=1704, size=9528, time=0.35s
[SABRE] Circuit 19 (TSP_Ncity-10): 2Q depth=1091, size=22041, time=0.38s
[SABRE] Circuit 20 (TSP_Ncity-10): 2Q depth=1091, size=22005, time=0.38s
[SABRE] Circuit 21 (ham-unary-color02-queen13_13_k-4): 2Q depth=224, size=8321, time=0.17s
[AI] Circuit 0 (all-vib-hc3h2cn): 2Q depth=2, size=258, time=0.17s
[AI] Circuit 1 (ham-graph-gnp_k-5): 2Q depth=323, size=4418, time=3.13s
[AI] Circuit 2 (TSP_Ncity-5): 2Q depth=161, size=2229, time=1.47s
[AI] Circuit 3 (tfim): 2Q depth=20, size=402, time=0.34s
[AI] Circuit 4 (all-vib-h2co): 2Q depth=38, size=661, time=0.19s
[AI] Circuit 5 (uuf100-ham): 2Q depth=391, size=5130, time=3.27s
[AI] Circuit 6 (uuf100-ham): 2Q depth=463, size=6095, time=4.23s
[AI] Circuit 7 (graph-gnp_k-4): 2Q depth=3207, size=25641, time=15.15s
[AI] Circuit 8 (uf100-ham): 2Q depth=637, size=8267, time=5.87s
[AI] Circuit 9 (uf100-ham): 2Q depth=632, size=9330, time=7.29s
[AI] Circuit 10 (TSP_Ncity-7): 2Q depth=452, size=7418, time=6.02s
[AI] Circuit 11 (all-vib-cyclo_propene): 2Q depth=38, size=1323, time=0.27s
[AI] Circuit 12 (TSP_Ncity-8): 2Q depth=609, size=11131, time=10.07s
[AI] Circuit 13 (uf100-ham): 2Q depth=2251, size=31128, time=38.77s
[AI] Circuit 14 (tfim): 2Q depth=165, size=3460, time=1.64s
[AI] Circuit 15 (flat100-ham): 2Q depth=91, size=3497, time=2.49s
[AI] Circuit 16 (graph-regular_reg-4): 2Q depth=664, size=15256, time=12.35s
[AI] Circuit 17 (tfim): 2Q depth=583, size=9157, time=6.28s
[AI] Circuit 18 (FH_D-1): 2Q depth=1193, size=7754, time=4.54s
[AI] Circuit 19 (TSP_Ncity-10): 2Q depth=1134, size=22577, time=25.64s
[AI] Circuit 20 (TSP_Ncity-10): 2Q depth=1172, size=23851, time=28.97s
[AI] Circuit 21 (ham-unary-color02-queen13_13_k-4): 2Q depth=219, size=8600, time=8.85s
[Rustiq] Circuit 0 (all-vib-hc3h2cn): 2Q depth=2, size=257, time=0.16s
[Rustiq] Circuit 1 (ham-graph-gnp_k-5): 2Q depth=640, size=5831, time=0.13s
[Rustiq] Circuit 2 (TSP_Ncity-5): 2Q depth=408, size=3985, time=0.08s
[Rustiq] Circuit 3 (tfim): 2Q depth=31, size=688, time=0.07s
[Rustiq] Circuit 4 (all-vib-h2co): 2Q depth=65, size=1058, time=2.91s
[Rustiq] Circuit 5 (uuf100-ham): 2Q depth=633, size=6757, time=0.14s
[Rustiq] Circuit 6 (uuf100-ham): 2Q depth=795, size=8495, time=0.17s
[Rustiq] Circuit 7 (graph-gnp_k-4): 2Q depth=13768, size=139793, time=2.92s
[Rustiq] Circuit 8 (uf100-ham): 2Q depth=1099, size=11878, time=0.25s
[Rustiq] Circuit 9 (uf100-ham): 2Q depth=911, size=11111, time=0.22s
[Rustiq] Circuit 10 (TSP_Ncity-7): 2Q depth=1183, size=13197, time=0.27s
[Rustiq] Circuit 11 (all-vib-cyclo_propene): 2Q depth=67, size=2491, time=13.56s
[Rustiq] Circuit 12 (TSP_Ncity-8): 2Q depth=1615, size=21358, time=0.48s
[Rustiq] Circuit 13 (uf100-ham): 2Q depth=2920, size=40465, time=0.91s
[Rustiq] Circuit 14 (tfim): 2Q depth=489, size=6552, time=0.15s
[Rustiq] Circuit 15 (flat100-ham): 2Q depth=378, size=5906, time=0.14s
[Rustiq] Circuit 16 (graph-regular_reg-4): 2Q depth=12163, size=168679, time=2.94s
[Rustiq] Circuit 17 (tfim): 2Q depth=1208, size=17042, time=0.36s
[Rustiq] Circuit 18 (FH_D-1): 2Q depth=1061, size=24000, time=0.47s
[Rustiq] Circuit 19 (TSP_Ncity-10): 2Q depth=2565, size=41340, time=1.38s
[Rustiq] Circuit 20 (TSP_Ncity-10): 2Q depth=2565, size=41275, time=1.38s
[Rustiq] Circuit 21 (ham-unary-color02-queen13_13_k-4): 2Q depth=808, size=17548, time=0.42s
print_summary_table(results_large)Output:
Mean +/- std per compilation method
Method 2Q Depth Gate Count Runtime (s)
------------------------------------------------------------------------------
SABRE 709.1 +/- 783.8 9,100.5 +/- 8,493.1 0.2 +/- 0.1
AI 656.6 +/- 777.5 9,435.6 +/- 8,853.0 8.5 +/- 10.2
Rustiq 2,062.5 +/- 3,631.1 26,804.8 +/- 43,403.1 1.3 +/- 2.9
Mean % improvement vs SABRE (positive = better than SABRE)
Method 2Q Depth Gate Count Runtime (s)
------------------------------------------------------------------------------
AI +9.6% +/- 22.8% -3.4% +/- 9.4% -3620.0% +/- 2405.5%
Rustiq -154.5% +/- 273.9% -137.1% +/- 233.2% -527.0% +/- 1405.5%
print_per_circuit_comparison(results_large, num_rows=8)Output:
2Q Depth (first 8 circuits by qubit count); * = best
Idx Circuit Q SABRE AI Rustiq
----------------------------------------------------
0 all-vib-hc3h2cn 24 2* 2* 2*
1 ham-graph-gnp_k- 24 345 323* 640
2 TSP_Ncity-5 25 187 161* 408
3 tfim 26 100 20* 31
4 all-vib-h2co 32 30* 38 65
5 uuf100-ham 40 414 391* 633
6 uuf100-ham 40 523 463* 795
7 graph-gnp_k-4 40 3028* 3207 13768
Gate Count (first 8 circuits by qubit count); * = best
Idx Circuit Q SABRE AI Rustiq
----------------------------------------------------
0 all-vib-hc3h2cn 24 258 258 257*
1 ham-graph-gnp_k- 24 4036* 4418 5831
2 TSP_Ncity-5 25 2045* 2229 3985
3 tfim 26 489 402* 688
4 all-vib-h2co 32 570* 661 1058
5 uuf100-ham 40 4779* 5130 6757
6 uuf100-ham 40 5667* 6095 8495
7 graph-gnp_k-4 40 24885* 25641 139793
Runtime (s) (first 8 circuits by qubit count); * = best
Idx Circuit Q SABRE AI Rustiq
----------------------------------------------------
0 all-vib-hc3h2cn 24 0.16 0.17 0.16*
1 ham-graph-gnp_k- 24 0.08* 3.13 0.13
2 TSP_Ncity-5 25 0.04* 1.47 0.08
3 tfim 26 0.21 0.34 0.07*
4 all-vib-h2co 32 0.18* 0.19 2.91
5 uuf100-ham 40 0.09* 3.27 0.14
6 uuf100-ham 40 0.11* 4.23 0.17
7 graph-gnp_k-4 40 0.39* 15.15 2.92
plot_transpilation_comparison(
results_large,
"Large-Scale Hamiltonian Circuits: Compilation Comparison",
)Output:
plot_pct_improvement_vs_sabre(
results_large,
"Large-Scale Hamiltonian Circuits",
)Output:
plot_best_method_bars(results_large)Output:
# Select circuit index 3 from the large-scale transpiled circuits
test_idx_large = 3
test_circuit_large = qc_large[test_idx_large]
print(
f"Test circuit: {test_circuit_large.name}, {test_circuit_large.num_qubits} qubits"
)
tqc_methods_large = {
"SABRE": tqc_sabre_large[test_idx_large],
"AI": tqc_ai_large[test_idx_large],
"Rustiq": tqc_rustiq_large[test_idx_large],
}
print(f"\nTranspilation metrics for circuit index {test_idx_large}:")
for method, tqc in tqc_methods_large.items():
depth_2q = tqc.depth(lambda x: x.operation.num_qubits == 2)
size = tqc.size()
print(f" {method:8s} 2Q depth={depth_2q:5d} size={size:6d}")Output:
Test circuit: tfim, 26 qubits
Transpilation metrics for circuit index 3:
SABRE 2Q depth= 100 size= 489
AI 2Q depth= 20 size= 402
Rustiq 2Q depth= 31 size= 688
pm_mirror = generate_preset_pass_manager(
optimization_level=0, backend=backend
)
for method, tqc in tqc_methods_large.items():
# print the count ops for each circuit
mirror = tqc.copy()
mirror.compose(tqc.inverse(), inplace=True)
mirror.measure_all()
mirror = pm_mirror.run(mirror)
print(f"\n{method} transpiled circuit:")
print(tqc.count_ops())
print(f"{method} mirror circuit count ops:")
print(mirror.count_ops())Output:
SABRE transpiled circuit:
OrderedDict({'sx': 211, 'rz': 163, 'cz': 104, 'x': 11})
SABRE mirror circuit count ops:
OrderedDict({'rz': 1170, 'sx': 422, 'cz': 208, 'measure': 156, 'x': 22, 'barrier': 1})
AI transpiled circuit:
OrderedDict({'sx': 165, 'rz': 162, 'cz': 68, 'x': 7})
AI mirror circuit count ops:
OrderedDict({'rz': 984, 'sx': 330, 'measure': 156, 'cz': 136, 'x': 14, 'barrier': 1})
Rustiq transpiled circuit:
OrderedDict({'sx': 316, 'rz': 225, 'cz': 140, 'x': 7})
Rustiq mirror circuit count ops:
OrderedDict({'rz': 1714, 'sx': 632, 'cz': 280, 'measure': 156, 'x': 14, 'barrier': 1})
# Build mirror circuits and submit to real hardware
# The inverse may introduce gates (e.g., sxdg) not in the backend's
# basis gate set, so we re-transpile the mirror circuit.
pm_mirror = generate_preset_pass_manager(
optimization_level=0, backend=backend
)
shots_hw = 10000
hw_jobs = {}
for method, tqc in tqc_methods_large.items():
mirror = tqc.copy()
mirror.compose(tqc.inverse(), inplace=True)
mirror.measure_all()
# Re-transpile at opt level 0 to decompose into basis gates
# without changing the layout or routing
mirror = pm_mirror.run(mirror)
sampler = SamplerV2(mode=backend)
sampler.options.environment.job_tags = ["TUT_CMHSC"]
job = sampler.run([mirror], shots=shots_hw)
hw_jobs[method] = job
print(f"{method}: submitted job {job.job_id()}")Output:
SABRE: submitted job d8gvgq66983c73dqe5og
AI: submitted job d8gvgqe6983c73dqe5pg
Rustiq: submitted job d8gvgqm6983c73dqe5q0
# Retrieve results and compute fidelities
fidelities_large = {}
for method, job in hw_jobs.items():
result = job.result()
counts = result[0].data.meas.get_counts()
n_qubits = backend.num_qubits
all_zeros = "0" * n_qubits
fidelity = counts.get(all_zeros, 0) / shots_hw
fidelities_large[method] = fidelity
print(
f"{method:8s} P(|00...0>) = {fidelity:.4f} "
f"({counts.get(all_zeros, 0)}/{shots_hw})"
)Output:
SABRE P(|00...0>) = 0.0005 (5/10000)
AI P(|00...0>) = 0.3267 (3267/10000)
Rustiq P(|00...0>) = 0.1845 (1845/10000)
plot_mirror_results(
tqc_methods_large, fidelities_large, test_circuit_large.name
)Output:
컴파일 결과 분석
위의 벤치마크는 AI 기반 트랜스파일러인 SABRE와 Rustiq를 Hamlib 컬렉션에 포함된 해밀토니안 시뮬레이션 회로에서 소규모 및 대규모로 비교한 결과입니다.
2-큐비트 처리 깊이와 게이트 수
전반적으로 볼 때, SABRE와 AI 기반 트랜스파일러가 가장 뛰어난 성능을 보였으며, 각각 서로 다른 평가 지표에서 선두를 차지했습니다. 지표 차트에서 볼 수 있듯이, SABRE는 대다수의 회로에서 게이트 수가 가장 적게 나오며 거의 모든 회로에서 가장 빠른 방법을 보여줍니다. 이는 삽입된 SWAP 게이트를 최소화하도록 설계된 휴리스틱과, 최근 이루어진 레이아웃 및 라우팅 최적화 결과와 일치합니다. 이 AI 기반 트랜스파일러는 대부분의 회로에서 가장 낮은 2-큐비트 깊이를 산출하며, 이는 회로 깊이를 목표로 하는 강화 학습 목표의 일부와 일치한다. 요약 표에서도 동일한 경향이 나타납니다. SABRE는 평균 게이트 수가 더 적은 반면, AI 트랜스파일러는 평균 2-큐비트 깊이가 더 낮습니다. 두 방법 모두 모든 회로 범위에 걸쳐 일관되고 신뢰할 수 있습니다.
합성을 위해 PauliEvolutionGate 특별히 설계된 Rustiq는 대규모 회로 중 극히 일부에서만 가장 뛰어난 결과를 보여줍니다. 이 방법의 평균 지표는 소수의 현저한 이상치에 의해 크게 왜곡되어 있으며, 이는 컴파일 비교 그래프에서 큰 급등 형태로 나타납니다. 이 그래프에서 Rustiq는 다른 방법들에 비해 훨씬 더 높은 심도와 게이트 수를 보여줍니다. 이러한 특이값을 제외하면, 이 알고리즘의 평균 성능은 SABRE 및 AI 기반 트랜스파일러의 성능에 훨씬 더 가까워질 것입니다.
핵심적인 점은 어떤 단일 방법도 모든 회로에서 압도적인 우위를 차지하지 않는다는 것이다. 각 방법은 특정 상황에서 다른 방법들보다 뛰어난 성능을 보이기 때문에, 사용 가능한 모든 도구를 시도해 보고 각 회로에 가장 적합한 결과를 선택하는 것이 좋습니다.
런타임
SABRE는 언제나 가장 빠른 방법입니다. Rustiq는 대체로 비슷한 속도로 실행되지만, 컴파일 시간이 현저히 더 오래 걸리는 예외적인 경우가 발생할 수 있습니다. 이는 대규모 테스트 결과에서 특히 두드러지는데, 소수의 회로로 인해 Rustiq의 실행 시간이 급증하는 현상이 나타납니다. 이러한 이상값들은 평균 실행 시간에 큰 영향을 미치므로, Rustiq의 경우 중앙값이 더 대표적인 요약 지표가 될 수 있습니다. AI 기반 트랜스파일러는 세 가지 중 가장 속도가 느리며, 회로가 더 크고 복잡해질수록 실행 시간이 눈에 띄게 늘어납니다.
미러 회로 결과
미러 회로 실험 결과, 예상된 경향을 확인했다. 즉, 2-큐비트 깊이가 더 낮고 게이트 수가 더 적은 방식일수록 잡음 조건에서 더 높은 충실도를 달성한다. 이는 노이즈가 많은 시뮬레이터(소규모)와 실제 하드웨어(대규모) 모두에서 성립합니다.
각 미러 회로 그래프는 전체가 아닌 단일 회로를 나타낸다는 점을 유의하시기 바랍니다. 위의 하드웨어 예제에서는 26-큐비트 tfim 회로 하나를 사용하는데, 이 경우 SABRE가 AI 기반 트랜스파일러나 Rustiq보다 훨씬 더 높은 2-큐비트 깊이를 산출하므로, 그에 따라 충실도도 훨씬 더 낮습니다. 이는 전체적인 결과를 대표하지는 않습니다. 대규모 회로 전체를 살펴보면, SABRE의 2-큐비트 깊이는 대개 AI 기반 트랜스파일러의 2-큐비트 깊이와 비슷한 수준이며, 두 방법은 각기 다른 평가 지표에서 우위를 보입니다(AI 기반 트랜스파일러는 2-큐비트 깊이에서, SABRE는 게이트 수와 실행 시간에서 우위를 보입니다). 단일 미러 결과는 전체 워크로드가 아닌 특정 회로의 두 배 규모 버전을 테스트하는 것이므로, 이를 전체적인 방법의 품질에 대한 최종 평가로 해석해서는 안 됩니다.
권장사항
모든 회로에 적용할 수 있는 단 하나의 최상의 트랜스파일레이션 전략은 존재하지 않습니다. 가장 적합한 선택은 회로 구조, 최적화 목표, 그리고 할당된 컴파일 시간에 따라 달라집니다:
- SABRE를 기본값으로 권장합니다. 이 제품은 빠르고 신뢰할 수 있으며, 다양한 회로에서 뛰어난 성능을 발휘합니다. 더 세밀한 조정을 위해 사용자는 레이아웃 및 라우팅 시뮬레이션 횟수를 늘릴 수 있습니다( SABRE 최적화 튜토리얼 참조).
- 컴파일 시간이 제약 요인이 아닐 때, 특히 2-큐비트 깊이를 최소화하는 것이 최우선 과제일 때는 AI 기반 트랜스파일러를 사용해 볼 가치가 있습니다. 이 벤치마크에서 테스트된 대부분의 대규모 회로에서 이 트랜스파일러가 가장 낮은 2-큐비트 깊이를 기록했습니다.
- Rustiq는 회로 설계에
PauliEvolutionGate특화되어 있으며, 특히 소규모 회로에서 매우 낮은 설계 복잡도와 적은 게이트 수를 가진 솔루션을 찾아낼 수 있습니다. 더 큰 회로의 경우 때때로 훨씬 더 큰 결과가 나올 수 있으므로, 이 방법을 기본 설정으로 삼기보다는 여러 가지 시도해 볼 방법 중 하나로 사용하는 것이 가장 좋습니다.
실제로는 사용 가능한 모든 방법을 실행한 뒤, 각 회로에 대해 가장 좋은 결과를 선택하는 것이 가장 좋은 방법입니다. 실제 하드웨어에서 실행 성능이 향상될 수 있는 잠재적 이점에 비하면, 여러 방법을 시도하는 데 드는 컴파일 오버헤드는 미미한 수준입니다.
다음 단계
이 튜토리얼이 도움이 되셨다면, 다음 내용에도 관심이 있으실 수 있습니다:
참조
[1] "LightSABRE: "가볍고 향상된 사브레 알고리즘". H. Zou, M. Treinish, K. Hartman, A. Ivrii, J. Lishman et al. https://arxiv.org/abs/2409.08368
[2] "강화 학습을 통한 실용적이고 효율적인 양자 회로 합성 및 트랜스파일링". 박사 크레머, V. 빌라, H. 백, I. 듀란, I. 파로, J. 크루즈- 베니토 외. https://arxiv.org/abs/2405.13196
[3]. “강화 학습을 활용한 파울리 네트워크 회로 합성”. A. Dubal, D. Kremer, S. Martiel, V. Villar, D. Wang, J. Cruz-Benito 외. https://arxiv.org/abs/2503.14448
[4] “해밀턴 시뮬레이션 회로의 더 빠르고 간결한 합성”. T. Goubault de Brugiere, S. Martiel 외 https://arxiv.org/abs/2404.03280