انتقل إلى المحتوى الرئيسي

تحسين التحويل البرمجي باستخدام SABRE

تقدير وقت الاستخدام: دقيقة واحدة على معالج Heron r2 (ملاحظة: هذا تقدير فحسب. قد يختلف وقت التشغيل الفعلي.)

نتائج التعلم

بعد إتمام هذا البرنامج التعليمي، ستفهم:

  • كيفية ضبط معاملات SABRE (layout_trials، swap_trials، max_iterations) لتحسين جودة التحويل البرمجي
  • المقايضات بين وقت التحويل البرمجي وجودة الدائرة (العمق وعدد البوابات)
  • كيفية تخصيص الأسلوب الإرشادي لتوجيه SABRE (basic، decay، lookahead) ومقارنة أدائها على العتاد

المتطلبات الأساسية

نقترح أن تكون على دراية بالمواضيع التالية قبل البدء في هذا البرنامج التعليمي:

الخلفية

يُحوِّل التحويل البرمجي الدوائر الكمية إلى أشكال متوافقة مع عتاد كمي محدد. وتتمثل مرحلتان رئيسيتان في اختيار تخطيط الكيوبت (تعيين الكيوبتات المنطقية إلى الكيوبتات الفيزيائية) وتوجيه البوابات (إدراج بوابات SWAP كي تحترم البوابات متعددة الكيوبتات اتصالية الجهاز).

SABRE (خوارزمية البحث الإرشادي ثنائي الاتجاه القائمة على SWAP) تُحسِّن التخطيط والتوجيه معًا. وهي فعّالة بشكل خاص للدوائر واسعة النطاق (أكثر من 100 كيوبت) على الأجهزة ذات خرائط الاقتران المعقدة، كمعالجات IBM® Heron. يُقلّص SABRE بوابات SWAP ويُخفّض عمق الدائرة، مما يُحسّن دقة التنفيذ. وتُقلّص التحسينات الأخيرة في خوارزمية LightSABRE أوقات التشغيل وعدد البوابات بشكل أكبر.

في هذا البرنامج التعليمي، ستُهيّئ أولًا SabreLayout بمعاملات مختلفة لتحسين دائرة GHZ صغيرة ومراقبة تأثيرها على دقة التنفيذ. ثم ستُقارن بين الأساليب الإرشادية لتوجيه SABRE على نطاق واسع على عتاد حقيقي.

المتطلبات

قبل البدء في هذا البرنامج التعليمي، تأكد من تثبيت ما يلي:

  • Qiskit SDK الإصدار 2.0 أو أحدث، مع دعم التصوير البياني
  • Qiskit Runtime الإصدار 0.22 أو أحدث (pip install qiskit-ibm-runtime)
  • Qiskit Aer (pip install qiskit-aer)

الإعداد

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import EstimatorOptions
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_aer.primitives import EstimatorV2 as AerEstimator
from qiskit.transpiler.passes import (
SabreLayout,
SabreSwap,
BarrierBeforeFinalMeasurements,
StarPreRouting,
)
from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.passmanager.flow_controllers import ConditionalController
import matplotlib.pyplot as plt
import numpy as np
import time

seed = 42

service = QiskitRuntimeService(
channel="ibm_cloud",
token="<YOUR_API_TOKEN>", # Replace with your actual API token
instance="<YOUR_INSTANCE_NAME>", # Replace with your instance name if needed
)
backend = service.least_busy(operational=True, simulator=False)

print(f"Using backend: {backend.name}")
Using backend: ibm_kingston

مثال على محاكاة صغيرة النطاق

في هذا القسم، يُستخدم محاكي بضوضاء مستند إلى نموذج الضوضاء للواجهة الخلفية الحقيقية لإظهار كيفية تأثير تهيئات SabreLayout المختلفة على جودة التحويل البرمجي ودقة التنفيذ. يتيح لك استخدام qiskit_aer مع نموذج ضوضاء مُشتق من بيانات معايرة العتاد الفعلي اختبار التحويل البرمجي دون استهلاك رصيد العتاد.

الخطوة 1: تعيين المدخلات الكلاسيكية إلى مسألة كمية

نبني دائرة GHZ ذات الطوبولوجيا النجمية بـ 15 كيوبتًا. الكيوبت الأول هو المحور، مع بوابات CNOT تربطه مباشرةً بكل كيوبت آخر. تُشكّل هذه الطوبولوجيا مشكلة تخطيط صعبة لأنها لا تُعيَّن بشكل تافه على خريطة اقتران الجهاز.

كما نُعرِّف عوامل ZZ لقياس ارتباطات التشابك Z0Zi\langle Z_0 Z_i \rangle عبر أزواج الكيوبتات.

ghz_star_topology.png

عندما تعرف بنية الدائرة

SABRE هو خوارزمية للأغراض العامة ولا تفترض أي شيء عن بنية الدائرة. بالنسبة لدائرة GHZ ذات الطوبولوجيا النجمية هذه، يُعرف في الواقع التوجيه الأمثل: تكتشف تمريرة StarPreRouting الدوائر الجزئية النجمية وتُعيد كتابتها كسلسلة خطية تُعيَّن مباشرةً على أي واجهة خلفية تمتلك مسارًا خطيًا طويلًا بما يكفي. يركز هذا البرنامج التعليمي على SABRE لأنه يعمل مع الدوائر الاعتباطية، ولكن إذا كنت تعلم أن دائرتك تمتلك بنية خاصة واضحة، فإن تطبيق تمريرة متخصصة مثل StarPreRouting قبل التوجيه يمكن أن يتفوق على أي بحث إرشادي.

num_qubits_sim = 15

# Create star-topology GHZ circuit
qc_sim = QuantumCircuit(num_qubits_sim)
qc_sim.h(0)
for i in range(1, num_qubits_sim):
qc_sim.cx(0, i)
qc_sim.measure_all()

# ZZ operators: Z on qubit 0 and qubit i, identity elsewhere
operator_strings_sim = [
"Z" + "I" * i + "Z" + "I" * (num_qubits_sim - 2 - i)
for i in range(num_qubits_sim - 1)
]
operators_sim = [SparsePauliOp(op) for op in operator_strings_sim]

الخطوة 2: تحسين المسألة لتنفيذها على العتاد الكمي

يستخدم المُعدّ الافتراضي optimization_level=3 بالفعل SabreLayout، لكن بإعدادات افتراضية متحفظة. لاستكشاف تأثير الإعدادات الأكثر صرامةً، تُستبدل تلك التمريرة بـSabreLayout مُخصصة مُهيّأة للبحث الأكثر قوةً، مع إبقاء كل التمريرات الأخرى في مرحلة التخطيط دون تعديل. كنقطة مقارنة رابعة، يحتفظ مدير تمريرات رابع بـSabreLayout الافتراضية ويضيف StarPreRouting إلى مرحلة التهيئة. تمريرة StarPreRouting هي تمريرة واعية بالبنية تكتشف الدوائر الجزئية النجمية وتُعيد كتابتها كسلسلة خطية قبل التوجيه.

سير العمل كالتالي:

  1. الفحص: فحص مدير التمريرات الافتراضي لمعرفة موقع SabreLayout داخل مرحلة layout.
  2. الاستبدال: استبدال تلك التمريرة بنسخة SabreLayout مُخصصة باستخدام PassManager.replace(index, passes=...)، وبناء متغير pm_star مع pm.init += StarPreRouting().
  3. التشغيل: تشغيل جميع مديري التمريرات الأربعة ومقارنة المقاييس.

التهيئات الأربع هي:

التهيئةالوصف
pm_1 (افتراضية)المستوى الافتراضي 3 (SabreLayout مع max_iterations=4، layout_trials=20، swap_trials=20)
pm_2SabreLayout مُخصصة (max_iterations=4، layout_trials=200، swap_trials=200)
pm_3SabreLayout مُخصصة (max_iterations=8، layout_trials=200، swap_trials=200)
pm_starالإعداد الافتراضي مع إضافة StarPreRouting إلى مرحلة التهيئة

معاملات SABRE الرئيسية:

  • layout_trials / swap_trials: تتحكم في عدد تخطيطات المرشحين وحلول التوجيه التي يستكشفها SABRE. زيادة عدد التجارب تعني أن SABRE يستكشف فضاءً بحثيًا أوسع، مما يزيد فرص إيجاد حل أفضل.
  • max_iterations: تتحكم في عدد دورات تنقية التوجيه ذهابًا وإيابًا التي يُنفّذها SABRE على كل مرشح. يُحسّن SABRE التخطيط تكراريًا من خلال التعلم من تغذية راجعة التوجيه، لذا فكلما زادت التكرارات، كلما كانت التحسينات أفضل.

يأتي كلا الأمرين على حساب وقت تحويل برمجي أطول، لكن الدوائر الناتجة أقصر وتستخدم بوابات أقل، مما يُقلّص إزالة الترابط وأخطاء البوابات مباشرةً على العتاد الحقيقي.

الخطوة 2أ: فحص مدير التمريرات الافتراضي. يتكون StagedPassManager من مراحل (init، layout، routing، translation، optimization، scheduling)، وكل منها هو PassManager بحد ذاته. يُصيّر استدعاء .draw() على مرحلة تمريراتها كرسم بياني حتى نتمكن من رؤية موقع SabreLayout.

# Build the default pass manager (no modifications yet)
pm_1 = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=seed
)

# Visualize the layout stage to see where SabreLayout sits
pm_1.layout.draw()

Output of the previous code cell

في الرسم البياني أعلاه، تمريرة SabreLayout التي نريد تخصيصها تقع داخل ConditionalController في الموضع [2] من مرحلة التخطيط. يقوم ذلك المتحكم بشيئين:

  • يُقيّد SabreLayout كي تعمل فقط عندما يفشل VF2Layout في [1] في إيجاد تعيين مثالي (وإلا يُحتفظ بتخطيط VF2 المثالي).
  • يُقدِّم SabreLayout بتمريرة BarrierBeforeFinalMeasurements التي تحمي القياسات من إعادة ترتيبها أثناء التوجيه الداخلي لـ SabreLayout.

إذا اكتفينا بـ replace(index=2, passes=sl_2)، فسيُفقَد كلا السلوكين. للاحتفاظ بهما، نُعيد تغليف SabreLayout المُخصصة في نفس ConditionalController (مع نفس الشرط والحاجز الواقي) قبل تبادلها.

الخطوة 2ب: بناء تمريرات SabreLayout المُخصصة واستبدال التمريرة الافتراضية.

cmap = backend.coupling_map

# Custom SabreLayout passes with more aggressive search
sl_2 = SabreLayout(
coupling_map=cmap,
seed=seed,
max_iterations=4,
layout_trials=200,
swap_trials=200,
)
sl_3 = SabreLayout(
coupling_map=cmap,
seed=seed,
max_iterations=8,
layout_trials=200,
swap_trials=200,
)

# Same condition the preset uses: only run SabreLayout when VF2Layout did not
# find a perfect mapping. This preserves any perfect layout VF2 produced at [1].
def _vf2_match_not_found(property_set):
if property_set["layout"] is None:
return True
return (
property_set["VF2Layout_stop_reason"] is not None
and property_set["VF2Layout_stop_reason"]
is not VF2LayoutStopReason.SOLUTION_FOUND
)

def wrap_sabre(sabre_pass):
"""Re-wrap a SabreLayout in the original ConditionalController + barrier."""
return ConditionalController(
[
BarrierBeforeFinalMeasurements(
"qiskit.transpiler.internal.routing.protection.barrier"
),
sabre_pass,
],
condition=_vf2_match_not_found,
)

# Build two fresh pass managers and swap in the wrapped custom SabreLayout at index 2
pm_2 = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=seed
)
pm_3 = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=seed
)
pm_2.layout.replace(index=2, passes=wrap_sabre(sl_2))
pm_3.layout.replace(index=2, passes=wrap_sabre(sl_3))

# Build pm_star: default preset with StarPreRouting added to the init stage
pm_star = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=seed
)
pm_star.init += StarPreRouting()

# Visualize pm_3 after replacement (pm_2 has the same structure, only max_iterations differs)
pm_3.layout.draw()

Output of the previous code cell

الموضع [2] الآن هو ConditionalController مرةً أخرى — متطابق في الشكل مع الافتراضي، لكن SabreLayout الداخلية هي نسختنا المُخصصة (مع layout_trials=200، swap_trials=200، وmax_iterations=8 لـpm_3؛ وpm_2 متطابق باستثناء max_iterations=4). يُحافظ الحاجز الواقي وتقييد _vf2_match_not_found على مكانيهما، لذا الفرق الوحيد بين pm_2/pm_3 وpm_1 هو تهيئة SABRE ذاتها. يحتفظ pm_star بـSabreLayout الافتراضية ويضيف فقط StarPreRouting في نهاية مرحلة التهيئة.

الخطوة 2ج: تشغيل كل مدير تمريرات والمقارنة بينها.

results_sim = {}
for name, pm in [
("pm_1 (4,20,20)", pm_1),
("pm_2 (4,200,200)", pm_2),
("pm_3 (8,200,200)", pm_3),
("pm_star (default + StarPreRouting)", pm_star),
]:
t0 = time.time()
tqc = pm.run(qc_sim)
elapsed = time.time() - t0
depth = tqc.depth(lambda x: x.operation.num_qubits == 2)
size = tqc.size()
ops_mapped = [op.apply_layout(tqc.layout) for op in operators_sim]
results_sim[name] = {
"tqc": tqc,
"ops": ops_mapped,
"depth": depth,
"size": size,
"time": elapsed,
}
print(f"{name}: 2Q Depth {depth}, Size {size}, Time {elapsed:.2f}s")

# Print improvement relative to default (pm_1)
baseline = results_sim["pm_1 (4,20,20)"]
print("\nImprovement vs. default (pm_1):")
for name in [
"pm_2 (4,200,200)",
"pm_3 (8,200,200)",
"pm_star (default + StarPreRouting)",
]:
r = results_sim[name]
depth_pct = (baseline["depth"] - r["depth"]) / baseline["depth"] * 100
size_pct = (baseline["size"] - r["size"]) / baseline["size"] * 100
print(f" {name}: 2Q depth {depth_pct:+.1f}%, size {size_pct:+.1f}%")
pm_1 (4,20,20): 2Q Depth 38, Size 183, Time 0.01s
pm_2 (4,200,200): 2Q Depth 36, Size 183, Time 0.15s
pm_3 (8,200,200): 2Q Depth 30, Size 158, Time 0.16s
pm_star (default + StarPreRouting): 2Q Depth 26, Size 160, Time 0.01s

Improvement vs. default (pm_1):
pm_2 (4,200,200): 2Q depth +5.3%, size +0.0%
pm_3 (8,200,200): 2Q depth +21.1%, size +13.7%
pm_star (default + StarPreRouting): 2Q depth +31.6%, size +12.6%

أنتجت مديرو التمريرات الثلاثة المُعدَّلة دوائر ذات عمق ثنائي الكيوبت أقل من الافتراضي. تُقايض تهيئات SABRE الأكثر قوةً (pm_2 وpm_3) وقت تحويل برمجي أطول مقابل بحث أوسع، بينما تستغل pm_star البنية النجمية للدائرة وتُنتج نتيجةً أكثر ضحالةً دون أن تدفع أي تكلفة إضافية للتحويل. ستتفاوت المكاسب الدقيقة من تشغيل لآخر، لكن الاتجاه العام متسق: المزيد من تجارب SABRE وتكراراته تتيح للبحث الإرشادي استكشاف فضاء أوسع، والتمريرات الواعية بالبنية كـStarPreRouting يمكنها تجاوز ذلك البحث كليًا عندما يتطابق شكل الدائرة.

حتى على هذا النطاق الصغير (15 كيوبتًا)، تكفي مساحة التحسين لأن تتفوق الأساليب الثلاثة على الافتراضي. مع الدوائر الأكبر (أكثر من 100 كيوبت)، يتنامى فضاء البحث بشكل كبير وتصبح فوائد زيادة التجارب والتمريرات الواعية بالبنية أكثر وضوحًا، كما سيُظهر القسم الخاص بالنطاق الواسع.

pm_names = list(results_sim.keys())
depths = [results_sim[n]["depth"] for n in pm_names]
sizes = [results_sim[n]["size"] for n in pm_names]
times = [results_sim[n]["time"] for n in pm_names]
colors = ["#404080", "#2a9d8f", "#a8d05e", "#e29bdd"]
x = np.arange(len(pm_names))

fig, axs = plt.subplots(1, 3, figsize=(14, 5))

# 2Q Depth
bars = axs[0].bar(x, depths, color=colors)
axs[0].set_ylabel("2Q Depth", fontsize=11)
axs[0].set_title("Two-Qubit Gate Depth", fontsize=13)
axs[0].set_ylim(0, max(depths) * 1.2)
for bar, val in zip(bars, depths):
axs[0].text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + max(depths) * 0.02,
str(val),
ha="center",
va="bottom",
fontsize=11,
fontweight="bold",
)
for i in range(1, len(depths)):
pct = (depths[0] - depths[i]) / depths[0] * 100
if pct != 0:
axs[0].text(
bars[i].get_x() + bars[i].get_width() / 2,
bars[i].get_height() / 2,
f"{pct:+.0f}%",
ha="center",
va="center",
fontsize=10,
color="white",
fontweight="bold",
)

# Size
bars = axs[1].bar(x, sizes, color=colors)
axs[1].set_ylabel("Gate Count", fontsize=11)
axs[1].set_title("Circuit Size", fontsize=13)
axs[1].set_ylim(0, max(sizes) * 1.2)
for bar, val in zip(bars, sizes):
axs[1].text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + max(sizes) * 0.02,
str(val),
ha="center",
va="bottom",
fontsize=11,
fontweight="bold",
)
for i in range(1, len(sizes)):
pct = (sizes[0] - sizes[i]) / sizes[0] * 100
if abs(pct) > 0.1:
axs[1].text(
bars[i].get_x() + bars[i].get_width() / 2,
bars[i].get_height() / 2,
f"{pct:+.0f}%",
ha="center",
va="center",
fontsize=10,
color="white",
fontweight="bold",
)

# Time
bars = axs[2].bar(x, times, color=colors)
axs[2].set_ylabel("Time (s)", fontsize=11)
axs[2].set_title("Transpilation Time", fontsize=13)
axs[2].set_ylim(0, max(times) * 1.3)
for bar, val in zip(bars, times):
axs[2].text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + max(times) * 0.03,
f"{val:.2f}s",
ha="center",
va="bottom",
fontsize=11,
fontweight="bold",
)

for ax in axs:
ax.set_xticks(x)
ax.set_xticklabels(pm_names, fontsize=8, rotation=15)
ax.grid(axis="y", linestyle="--", alpha=0.5)

plt.suptitle(
"Transpilation quality vs. configuration",
fontsize=14,
fontweight="bold",
y=1.02,
)
plt.tight_layout()
plt.show()

Output of the previous code cell

الخطوة 3: التنفيذ باستخدام أوليات Qiskit

نشغّل كل دائرة محوَّلة 10 مرات باستخدام Aer EstimatorV2 مع نموذج ضوضاء مُشتق من الواجهة الخلفية الحقيقية. نظرًا لأن نتائج المحاكاة بالضوضاء تتفاوت بين التشغيلات، فإن الحساب المتوسط عبر تشغيلات متعددة يُعطي تقديرات أكثر موثوقية للدقة ويتيح لنا قياس عدم اليقين الإحصائي باستخدام أشرطة الخطأ.

# Create a noisy estimator from the real backend's noise model
noisy_estimator = AerEstimator.from_backend(backend)

num_runs = 10
# sim_all_runs[name] = list of arrays, one per run
sim_all_runs = {name: [] for name in results_sim}

for run in range(num_runs):
for name, r in results_sim.items():
job = noisy_estimator.run([(r["tqc"], r["ops"])])
evs = list(job.result()[0].data.evs)
sim_all_runs[name].append(evs)
print(f"Run {run + 1}/{num_runs} done")

# Compute mean and std across runs for each config
sim_stats = {}
for name in results_sim:
all_evs = np.array(sim_all_runs[name]) # shape (num_runs, num_operators)
sim_stats[name] = {
"mean": np.mean(all_evs, axis=0),
"std": np.std(all_evs, axis=0),
"overall_mean": np.mean(all_evs),
"overall_std": np.std(
np.mean(all_evs, axis=1)
), # std of per-run averages
}
print(
f"{name}: mean fidelity = {sim_stats[name]['overall_mean']:.4f} +/- {sim_stats[name]['overall_std']:.4f}"
)
Run 1/10 done
Run 2/10 done
Run 3/10 done
Run 4/10 done
Run 5/10 done
Run 6/10 done
Run 7/10 done
Run 8/10 done
Run 9/10 done
Run 10/10 done
pm_1 (4,20,20): mean fidelity = 0.9510 +/- 0.0094
pm_2 (4,200,200): mean fidelity = 0.9513 +/- 0.0043
pm_3 (8,200,200): mean fidelity = 0.9540 +/- 0.0065
pm_star (default + StarPreRouting): mean fidelity = 0.9547 +/- 0.0072

بما أن هذه دائرة صغيرة، تقع قيم الدقة قريبةً من بعضها عبر جميع التهيئات الأربع. الدوائر قصيرة بما يكفي بحيث لا يُعاقب ضجيج العتاد حتى النسخة الأقل تحسينًا بشدة. تتوافق الدقة المتوسطة على نطاق واسع مع عمق 2Q: pm_3 وpm_star، الدائرتان الأكثر ضحالةً، تحققان أعلى دقة وتتعادلان بشكل جوهري في حدود أشرطة خطأهما. pm_2 هو مثال مضاد مفيد: على الرغم من أن عمق 2Q فيه أقل من pm_1، تنتهي دقته المتوسطة بأن تكون أدنى قليلًا أيضًا، مما يُذكّرنا بأن العلاقة بين العمق والدقة إحصائية لا حتمية. الكيوبتات الفيزيائية التي يختارها التخطيط ومعايرتها وقت التشغيل تلعب دورًا أيضًا.

الخطوة 4: المعالجة اللاحقة وإرجاع النتيجة بالتنسيق الكلاسيكي المطلوب

بعد ذلك، رسِّم ارتباطات التشابك Z0Zi\langle Z_0 Z_i \rangle بدلالة مسافة الكيوبت، إلى جانب متوسط الارتباط بوصفه مقياسًا وحيدًا للدقة. في الحالة المثالية (بدون ضوضاء)، ستكون جميع الارتباطات تساوي 1. مع الضوضاء الواقعية، تُدخل كل بوابة إضافية خطأً ويُتيح كل خطوة زمنية إضافية إزالة الترابط، لذا يجب أن تُحافظ الدائرة المحوَّلة برمجيًا ذات العمق الأقل وعدد البوابات الأقل (خاصةً البوابات ثنائية الكيوبت) على التشابك بشكل أفضل.

data_sim = list(range(1, len(operators_sim) + 1))
markers = ["o", "s", "^", "*"]
colors_line = ["#404080", "#2a9d8f", "#a8d05e", "#e29bdd"]

fig, (ax1, ax2) = plt.subplots(
1, 2, figsize=(14, 5), gridspec_kw={"width_ratios": [2.5, 1]}
)

# Left: correlations vs distance with error bars (mean +/- 1 std)
for (name, stats), marker, color in zip(
sim_stats.items(), markers, colors_line
):
ax1.errorbar(
data_sim,
stats["mean"],
yerr=stats["std"],
marker=marker,
label=name,
color=color,
linewidth=2,
capsize=3,
capthick=1,
elinewidth=1,
)

ax1.set_xlabel("Distance between qubits $i$", fontsize=11)
ax1.set_ylabel(r"$\langle Z_0 Z_i \rangle$", fontsize=11)
ax1.set_title(
"Entanglement correlations vs. qubit distance (avg. of 10 runs)",
fontsize=12,
)
ax1.legend(fontsize=9)
ax1.grid(alpha=0.3)

# Right: mean correlation bar chart with error bars
names = list(sim_stats.keys())
means = [sim_stats[n]["overall_mean"] for n in names]
stds = [sim_stats[n]["overall_std"] for n in names]
x_bar = np.arange(len(names))
bars = ax2.bar(
x_bar, means, yerr=stds, color=colors_line, capsize=5, ecolor="gray"
)
ax2.set_ylabel(r"Mean $\langle Z_0 Z_i \rangle$", fontsize=11)
ax2.set_title("Average fidelity", fontsize=13, pad=12)
y_range = max(means) - min(means) if max(means) != min(means) else 0.01
# Top of ylim accounts for the bar height + std error bar + headroom for the value label
y_top = max(m + s for m, s in zip(means, stds)) + y_range * 1.5
ax2.set_ylim(min(means) - y_range * 0.8, y_top)
for bar, val, std in zip(bars, means, stds):
ax2.text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + std + y_range * 0.15,
f"{val:.4f}",
ha="center",
va="bottom",
fontsize=10,
fontweight="bold",
)
# Annotate % change vs pm_1
baseline_mean = means[0]
for i in range(1, len(means)):
pct = (means[i] - baseline_mean) / baseline_mean * 100
if abs(pct) > 0.01:
mid_y = (means[i] + ax2.get_ylim()[0]) / 2
ax2.text(
bars[i].get_x() + bars[i].get_width() / 2,
mid_y,
f"{pct:+.1f}%",
ha="center",
va="center",
fontsize=10,
color="white",
fontweight="bold",
)
ax2.set_xticks(x_bar)
ax2.set_xticklabels(names, fontsize=8, rotation=15)
ax2.grid(axis="y", linestyle="--", alpha=0.5)

fig.tight_layout()
plt.show()

Output of the previous code cell

تُظهر النتائج صلةً واضحة بين جودة التحويل البرمجي ودقة التنفيذ، مع بعض التحفظات المفيدة:

  • pm_1 (افتراضي): خط الأساس. مع 20 تجربة فقط وأربع تكرارات، يملك SABRE مجالًا محدودًا للتحسين، مما يُنتج أعمق الدوائر بين دوائر SABRE فحسب.
  • pm_2 (مزيد من التجارب): يكتشف استكشاف عشرة أضعاف المرشحين تخطيطًا أقل عمقًا قليلًا، لكن متوسط الدقة يظل ثابتًا تقريبًا (وقد يقل حتى عن الأساس في حدود الضوضاء) لأن مكسب العمق صغير على هذا النطاق.
  • pm_3 (مزيد من التجارب + مزيد من التكرارات): مضاعفة max_iterations إلى 8 تمنح SABRE المزيد من دورات التنقية، مما يُنتج أكثر دوائر SABRE ضحالةً وأعلى متوسط دقة في المقارنة.
  • pm_star (افتراضي + StarPreRouting): تُضيف StarPreRouting إلى مرحلة التهيئة لإعداد افتراضي بخلاف ذلك. تُطوي إعادة الكتابة الواعية بالبنية النجمةَ إلى سلسلة خطية يُعيّنها باقي المحوّل البرمجي على المسار الخطي للجهاز، مما يُنتج أكثر الدوائر ضحالةً في المجمل (أفضل قليلًا من pm_3) ويتعادل مع pm_3 في الدقة ضمن أشرطة الخطأ. يُحقق ذلك بنفس وقت التحويل كالافتراضي، إذ إن إعادة الكتابة مجانية عمليًا مقارنةً بالبحث العشوائي لـ SABRE.

لاحظ أن زيادة max_iterations ليس لها دائمًا تأثير إيجابي. في هذه الحالة ساعدت بشكل كبير، لكن لدوائر أو واجهات خلفية أخرى قد لا تُفضي التكرارات الإضافية إلى مزيد من التحسين، أو قد تُضر قليلًا بالأداء بسبب الإفراط في تحسين الحد الأدنى المحلي. بشكل عام، يجب رفع layout_trials وswap_trials بقدر ما يسمح ميزانيتك الزمنية، إذ المزيد من التجارب يزيد دائمًا فرصة إيجاد تخطيط أفضل. رفع max_iterations يستحق الاختبار لكن يجب التحقق منه لحالة الاستخدام المحددة. التمريرات المتخصصة كـStarPreRouting مماثلة في الجوهر لكن أكثر اعتمادًا على الدائرة: فهي تساعد فقط عندما تحتوي الدائرة فعلًا على البنية التي تستهدفها. المكسب كبير عند انطباقها وصفر بخلاف ذلك، لكنها لا تكلف شيئًا يُذكر للمحاولة.

مثال على عتاد واسع النطاق

إضافةً إلى ضبط أعداد التجارب، يدعم SABRE تخصيص الأسلوب الإرشادي للتوجيه. يُقدّم SABRE ثلاثة أساليب إرشادية:

  • basic: نهج جشع بسيط يختار التبادل الذي يُقلّل المسافة الفورية للبوابة التالية.
  • decay (افتراضي): يُوزّن الكيوبتات ديناميكيًا بناءً على نشاطها الأخير، ويُثبّط التبادلات المتكررة على الكيوبتات ذاتها.
  • lookahead: يُقيّم تكاليف التوجيه المستقبلية بالنظر إلى البوابات القادمة، ومن المحتمل أن يجد تسلسلات تبادل أفضل.

لاستخدام أسلوب إرشادي مُخصص، أنشئ تمريرة SabreSwap وصلها بـSabreLayout عبر معامل routing_pass.

يُضاف مدير تمريرات رابع إلى المقارنة: pm_star_hw، الذي يحتفظ بالإعدادات الافتراضية لـSabreLayout/SabreSwap لكن يضيف StarPreRouting إلى مرحلة التهيئة. على هذا النطاق (100 كيوبت) يكون بحث SABRE أصعب، وتُصبح إعادة كتابة النجمة إلى سلسلة خطية فوزًا واضحًا لأن معالج Heron يمتلك مسارات خطية طويلة بما يكفي لاستيعاب الدائرة الناتجة.

هنا نُقارن بين الأساليب الإرشادية الثلاثة لـ SABRE إضافةً إلى StarPreRouting على نطاق واسع في دائرة GHZ بـ 100 كيوبت. نُجري تجارب تخطيط متعددة ببذور مختلفة لتهيئات SABRE، ونختار أفضل دائرة محوَّلة من كل منها، ونُرسلها جميعًا إلى العتاد الحقيقي مع نتيجة StarPreRouting.

الخطوات 1-4 مُضغطة في كتلة كود واحدة

هنا يُجمَع سير العمل الكامل على نطاق أكبر. عند استخدام SabreSwap كـrouting_pass لـSabreLayout، تُنفَّذ تجربة تخطيط واحدة فقط لكل استدعاء، لذا تتكرر خلية الكود التالية عبر البذور لاستكشاف فضاء التخطيط.

نستخدم نفس مساعد wrap_sabre المُعرَّف في الخطوة الصغيرة 2 (أعلاه)، ونُضيف مساعد wrap_routing مماثلًا لأن مرحلة routing في الفهرس [1] هي أيضًا ConditionalController([BarrierBeforeFinalMeasurements, routing_pass], ...) — استبدالها مباشرةً سيُسقط الحاجز الواقي وتقييد _swap_condition بالمثل.

# -------------------------Step 1-------------------------

num_qubits = 100

# Create star-topology GHZ circuit
qc = QuantumCircuit(num_qubits)
qc.h(0)
for i in range(1, num_qubits):
qc.cx(0, i)
qc.measure_all()

# ZZ operators
operator_strings = [
"Z" + "I" * i + "Z" + "I" * (num_qubits - 2 - i)
for i in range(num_qubits - 1)
]
operators = [SparsePauliOp(op) for op in operator_strings]
# -------------------------Step 2-------------------------

num_seeds = 10
seed_list = [seed + i for i in range(num_seeds)]
swap_trials = 200

# The default routing[1] is a ConditionalController([barrier, routing_pass],
# condition=_swap_condition); we re-wrap so the new routing pass keeps the
# protective barrier and is skipped when routing isn't needed (matches the preset).
def _swap_condition(property_set):
return not property_set["routing_not_needed"]

def wrap_routing(routing_pass):
return ConditionalController(
[
BarrierBeforeFinalMeasurements(
"qiskit.transpiler.internal.routing.protection.barrier"
),
routing_pass,
],
condition=_swap_condition,
)

heuristic_results = {}

# Three SABRE heuristics, swept over seeds
for heuristic in ["basic", "decay", "lookahead"]:
trials = []
for s in seed_list:
sr = SabreSwap(
coupling_map=cmap, heuristic=heuristic, trials=swap_trials, seed=s
)
sl = SabreLayout(coupling_map=cmap, routing_pass=sr, seed=s)
pm = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=s
)
# Re-wrap each custom pass in its original ConditionalController + barrier
# (wrap_sabre is defined in the small-scale Step 2 cell above).
pm.layout.replace(index=2, passes=wrap_sabre(sl))
pm.routing.replace(index=1, passes=wrap_routing(sr))

t0 = time.time()
tqc = pm.run(qc)
elapsed = time.time() - t0
depth = tqc.depth(lambda x: x.operation.num_qubits == 2)
size = tqc.size()
trials.append(
{
"tqc": tqc,
"depth": depth,
"size": size,
"time": elapsed,
"seed": s,
}
)

heuristic_results[heuristic] = trials

# Default preset + StarPreRouting in init, also swept over seeds for a fair comparison
star_trials = []
for s in seed_list:
pm_star_hw = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=s
)
pm_star_hw.init += StarPreRouting()

t0 = time.time()
tqc = pm_star_hw.run(qc)
elapsed = time.time() - t0
depth = tqc.depth(lambda x: x.operation.num_qubits == 2)
size = tqc.size()
star_trials.append(
{
"tqc": tqc,
"depth": depth,
"size": size,
"time": elapsed,
"seed": s,
}
)
heuristic_results["StarPreRouting"] = star_trials

# Print summary for each entry
for label in ["basic", "decay", "lookahead", "StarPreRouting"]:
trials = heuristic_results[label]
depths = [t["depth"] for t in trials]
sizes = [t["size"] for t in trials]
best = min(trials, key=lambda t: t["depth"])
print(f"{label}:")
print(
f" 2Q depth: min: {min(depths)}, mean: {np.mean(depths):.1f}, std: {np.std(depths):.1f}"
)
print(
f" size : min: {min(sizes)}, mean: {np.mean(sizes):.1f}, std: {np.std(sizes):.1f}"
)
print(
f" best seed: {best['seed']} (2Q depth={best['depth']}, size={best['size']})"
)
basic:
2Q depth: min: 524, mean: 570.5, std: 39.9
size : min: 3819, mean: 4227.1, std: 360.6
best seed: 51 (2Q depth=524, size=3852)
decay:
2Q depth: min: 387, mean: 436.4, std: 41.7
size : min: 2687, mean: 3183.1, std: 459.3
best seed: 45 (2Q depth=387, size=2786)
lookahead:
2Q depth: min: 364, mean: 424.6, std: 36.5
size : min: 2335, mean: 3014.6, std: 388.1
best seed: 51 (2Q depth=364, size=2485)
StarPreRouting:
2Q depth: min: 196, mean: 196.0, std: 0.0
size : min: 1151, mean: 1151.0, std: 0.0
best seed: 42 (2Q depth=196, size=1151)
hw_colors = {
"basic": "#ff7f0e",
"decay": "#d62728",
"lookahead": "#1f77b4",
"StarPreRouting": "#2a9d8f",
}

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 5))

for label in ["basic", "decay", "lookahead", "StarPreRouting"]:
trials = heuristic_results[label]
depths = [t["depth"] for t in trials]
sizes = [t["size"] for t in trials]
seeds = [t["seed"] for t in trials]
color = hw_colors[label]

ax1.scatter(
seeds,
depths,
label=label,
color=color,
alpha=0.8,
edgecolor="k",
s=60,
)
ax1.axhline(np.mean(depths), color=color, linestyle="--", alpha=0.5)

ax2.scatter(
seeds,
sizes,
label=label,
color=color,
alpha=0.8,
edgecolor="k",
s=60,
)
ax2.axhline(np.mean(sizes), color=color, linestyle="--", alpha=0.5)

ax1.set_xlabel("Seed", fontsize=11)
ax1.set_ylabel("2Q Depth", fontsize=11)
ax1.set_title("Two-Qubit Gate Depth per Seed", fontsize=13)
ax1.legend(fontsize=10)
ax1.grid(alpha=0.3)

ax2.set_xlabel("Seed", fontsize=11)
ax2.set_ylabel("Gate Count", fontsize=11)
ax2.set_title("Circuit Size per Seed", fontsize=13)
ax2.legend(fontsize=10)
ax2.grid(alpha=0.3)

plt.suptitle(
"Transpilation variability across seeds: SABRE heuristics vs. StarPreRouting",
fontsize=14,
fontweight="bold",
y=1.02,
)
plt.tight_layout()
plt.show()

# Summary comparison
for label in ["basic", "decay", "lookahead", "StarPreRouting"]:
best = min(heuristic_results[label], key=lambda t: t["depth"])
print(
f"{label}: best 2Q depth={best['depth']}, size={best['size']} (seed={best['seed']})"
)

Output of the previous code cell

basic: best 2Q depth=524, size=3852 (seed=51)
decay: best 2Q depth=387, size=2786 (seed=45)
lookahead: best 2Q depth=364, size=2485 (seed=51)
StarPreRouting: best 2Q depth=196, size=1151 (seed=42)
# -------------------------Step 3: Execute on hardware-------------------------

best_circuits = {}
for label in ["basic", "decay", "lookahead", "StarPreRouting"]:
best_circuits[label] = min(
heuristic_results[label], key=lambda t: t["depth"]
)
b = best_circuits[label]
print(f"Best {label}: 2Q depth={b['depth']}, size={b['size']}")

options = EstimatorOptions()
options.resilience_level = 2
options.dynamical_decoupling.enable = True
options.dynamical_decoupling.sequence_type = "XY4"
estimator = Estimator(backend, options=options)

hw_jobs = {}
hw_ops = {}
for label, best in best_circuits.items():
hw_ops[label] = [op.apply_layout(best["tqc"].layout) for op in operators]
hw_jobs[label] = estimator.run([(best["tqc"], hw_ops[label])])
print(f"{label} job: {hw_jobs[label].job_id()}")
estimator.options.environment.job_tags = ["TUT_TOWS"]

hw_results = {}
for label, job in hw_jobs.items():
hw_results[label] = job.result()[0]
print(f"{label} job done")
Best basic: 2Q depth=524, size=3852
Best decay: 2Q depth=387, size=2786
Best lookahead: 2Q depth=364, size=2485
Best StarPreRouting: 2Q depth=196, size=1151
basic job: d81q5tnoha1c73bknprg
decay job: d81q5tugbeec73aktopg
lookahead job: d81q5to0bvlc73d1epe0
StarPreRouting job: d81q5u7tjchs73bn82hg
basic job done
decay job done
lookahead job done
StarPreRouting job done
# -------------------------Step 4: Post-process-------------------------

data = list(range(1, len(operators) + 1))
hw_markers = {
"basic": "D",
"decay": "o",
"lookahead": "s",
"StarPreRouting": "*",
}
hw_labels = ["basic", "decay", "lookahead", "StarPreRouting"]

fig, (ax1, ax2) = plt.subplots(
1, 2, figsize=(14, 5), gridspec_kw={"width_ratios": [2.5, 1]}
)

# Left: correlations vs distance
for label in hw_labels:
evs = list(hw_results[label].data.evs)
b = best_circuits[label]
ax1.plot(
data,
evs,
marker=hw_markers[label],
color=hw_colors[label],
linewidth=2,
label=f"{label} (2Q depth={b['depth']}, size={b['size']})",
markersize=5 if label == "StarPreRouting" else 4,
)

ax1.set_xlabel("Distance between qubits $i$", fontsize=11)
ax1.set_ylabel(r"$\langle Z_0 Z_i \rangle$", fontsize=11)
ax1.set_title(
"Entanglement correlations vs. qubit distance (hardware)", fontsize=12
)
ax1.legend(fontsize=9)
ax1.grid(alpha=0.3)

# Right: mean fidelity bar chart
hw_means = [np.mean(list(hw_results[label].data.evs)) for label in hw_labels]
hw_bar_colors = [hw_colors[label] for label in hw_labels]
x_bar = np.arange(len(hw_labels))
bars = ax2.bar(x_bar, hw_means, color=hw_bar_colors)
ax2.set_ylabel(r"Mean $\langle Z_0 Z_i \rangle$", fontsize=11)
ax2.set_title("Average fidelity", fontsize=13)
y_range = (
max(hw_means) - min(hw_means) if max(hw_means) != min(hw_means) else 0.01
)
ax2.set_ylim(min(hw_means) - y_range * 0.2, max(hw_means) + y_range * 0.15)
for bar, val in zip(bars, hw_means):
ax2.text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + y_range * 0.05,
f"{val:.4f}",
ha="center",
va="bottom",
fontsize=11,
fontweight="bold",
)
ax2.set_xticks(x_bar)
ax2.set_xticklabels(hw_labels, fontsize=9, rotation=15)
ax2.grid(axis="y", linestyle="--", alpha=0.5)

fig.tight_layout()
plt.show()

print("\nMean fidelity:")
for label, m in zip(hw_labels, hw_means):
print(f" {label}: {m:.4f}")

Output of the previous code cell

Mean fidelity:
basic: 0.0344
decay: 0.1298
lookahead: 0.1857
StarPreRouting: 0.3295

التحليل

تُظهر مخططات التشتت تباينًا ملحوظًا عبر البذور لجميع الأساليب الإرشادية الثلاثة لـ SABRE، مما يُؤكد أهمية تشغيل تجارب تخطيط متعددة بدلًا من الاعتماد على تحويل برمجي واحد. خط StarPreRouting مستوٍ جوهريًا عبر البذور لأن إعادة كتابة النجمة إلى سلسلة خطية حتمية نظرًا للبنية؛ ثم يملك توجيه SABRE اللاحق قدرًا ضئيلًا جدًا من الحرية على السلسلة الخطية، لذا لا يكاد يكون للبذرة أي تأثير على العمق أو الحجم النهائيين.

من نتائج التحويل البرمجي، يتفوق كلا الأسلوبين الإرشاديين decay وlookahead باستمرار على basic بفارق كبير. يستخدم الأسلوب الإرشادي basic، رغم سرعته، استراتيجية جشعة بسيطة تؤدي في الغالب إلى دوائر أعمق بكثير. لدائرة GHZ ذات الطوبولوجيا النجمية هذه، يميل lookahead إلى إنتاج أدنى عمق ثنائي الكيوبت وعدد بوابات بين الأساليب الإرشادية لـ SABRE، إذ إن دالة تكلفته التطلعية مناسبة جيدًا للدوائر ذات أنماط الاتصالية بعيدة المدى. غير أن StarPreRouting يتجاوز الثلاثة بهامش كبير: بإعادة كتابة النجمة إلى سلسلة خطية قبل التوجيه، يتجاوز مشكلة البحث كليًا ويُسلّم دائرةً يستطيع باقي المحوّل البرمجي تعيينها على مسار خطي مع تبادلات SWAP إضافية ضئيلة.

تنتقل تلك الميزة مباشرةً إلى دقة العتاد. لا تترجم دائمًا عمق 2Q الأقل وعدد البوابات الأقل بشكل متساوٍ إلى دقة أعلى (الكيوبتات الفيزيائية التي يستخدمها التخطيط ومعايرتها وقت التشغيل تلعب دورًا أيضًا)، لكن عندما يكون الفجوة في العمق بين SABRE وStarPreRouting كبيرة كما هي هنا، يفوز النهج الواعي بالبنية بشكل حاسم لأن الدائرة تتراكم فيها إزالة الترابط وأحداث خطأ ثنائي الكيوبت بشكل أقل بكثير. يُظهر مخطط أعمدة الدقة StarPreRouting متقدمًا بشكل كبير حتى على أفضل الأساليب الإرشادية لـ SABRE، بينما يجلس basic دون البقية بسبب دوائره الأعمق بكثير التي تتراكم فيها أكثر الأخطاء.

الخلاصات الرئيسية:

  • بين الأساليب الإرشادية لـ SABRE، يتفوق decay وlookahead بشكل ملحوظ على basic للدوائر غير التافهة. يُفضّل أحد الأسلوبين للأعباء الإنتاجية.
  • يعتمد أفضل أسلوب إرشادي لـ SABRE على دائرتك وعتادك. اختبار أساليب إرشادية متعددة ببذور متعددة هو الاستراتيجية الأكثر موثوقية.
  • إذا أردت استكشاف تخطيطات أكثر، فزِد swap_trialslayout_trials عندما لا تُثبّت تمريرة توجيه مُخصصة) بدلًا من توزيع العمل على عُقد بعيدة. تمريرات SABRE بالفعل تُوازي التجارب عبر خيوط محلية، والعمل لكل تجربة صغير بما يكفي لأن يُهيمن عليه عادةً الحمل الزائد للتوزيع.
  • عندما تمتلك الدائرة بنية خاصة معروفة، يمكن لتطبيق تمريرة واعية بالبنية مثل StarPreRouting قبل SABRE أن يُحقق تحسينًا بمقدار رتبة، لا يستطيع أي قدر من ضبط SABRE مجاراته. هذا ليس بديلًا عن SABRE: StarPreRouting تساعد فقط عندما تحتوي الدائرة فعلًا على دوائر جزئية نجمية وتمتلك الواجهة الخلفية مسارًا خطيًا طويلًا بما يكفي. يستحق فحص مكتبة التمريرات للتطابقات متى عرفت شكل دائرتك.

الخطوات التالية

إذا وجدت هذا العمل مثيرًا للاهتمام، فقد تهمك المواد التالية:

توصيات

استبيان البرنامج التعليمي

يُرجى إجراء هذا الاستبيان القصير لتقديم ملاحظاتك حول هذا البرنامج التعليمي. ستُساعدنا تعليقاتك في تحسين محتوى موادنا التعليمية وتجربة المستخدم.

ملاحظة: هذا الاستبيان من IBM Quantum ويتعلق بـ محتوى البرنامج التعليمي (المكتوب من قبل IBM). توفر doQumentation الموقع والترجمات وتنفيذ الكود — لتقديم ملاحظات عليها، يرجى فتح مشكلة على GitHub.

Link to survey