انتقل إلى المحتوى الرئيسي
هذا المشروع في مرحلة تجريبية. وجدت خطأ؟ افتح مشكلة على GitHub. لديك سؤال أو فكرة؟ اسأل في Discussions — نحب سماع ملاحظاتك!
كيف جودة الترجمة في هذه الصفحة؟

كشف الأخطاء منخفض الحمل باستخدام رموز الزمكان

في هذا الدرس، سننفّذ سير عمل لأخذ عينات من دائرة كليفورد ونستخدم حزمة qiskit-paulice لتعزيز دقة التوزيع المقيس عن طريق تطبيق فحوصات باولي الزمكانية على امتداد الدائرة. تشبه فحوصات باولي الزمكانية فحوصات باولي المتماسكة (CPC) من حيث أن دائرة كليفورد "الحمولة" تُشبَك مع بعض الكيوبتات المساعدة من أجل "فحص" ثوابت معينة في الدائرة. ينتج عن قياس الكيوبتات المساعدة متلازمة (syndrome) تُستخدم لتحديد ما إذا كان قد تم اكتشاف خطأ منطقي أثناء تنفيذ الدائرة. وإبقاء العينات التي لم يُكتشف فيها أي خطأ فقط (postselection) يمكن أن يحسّن دقة التوزيعات المأخوذة، على حساب انخفاض معدلات الانتقاء. والفرق الأساسي بين CPC والفحوصات الزمكانية هو أن قياس متلازمات CPC يتطلب قياس مؤثرات عالية الوزن ومحلية زمنيًا. وفي طوبولوجيات الكيوبتات محدودة الاتصال، مثل heavy hex، يتطلب تنفيذ هذه الفحوصات الكثير من بوابات SWAP، مما يجعل الدائرة غالبًا أعمق من أن تُشغَّل عمليًا. أما تنفيذ فحوصات باولي كرموز زمكانية فيسمح بتوزيعها على امتداد دائرة الحمولة، مما يتيح ترميزًا للكيوبتات فعّالًا من ناحية العتاد ويبقى فعّالًا في اكتشاف الأخطاء المنطقية أثناء تنفيذ الدائرة. والدور الأساسي لحزمة qiskit-paulice هو إيجاد فحوصات باولي زمكانية صالحة وإدراجها في مواضع الدائرة التي تعظّم فعالية اكتشاف الأخطاء وتقلّل في الوقت نفسه الحمل الإضافي على الكيوبتات.

ينفّذ هذا الدرس سير عمل لتعزيز دقة التوزيع المأخوذ من دائرة كمومية باستخدام الفحوصات الزمكانية. وبما أننا سنستخدم دائرة كليفورد، يمكننا تقدير الدقة مباشرةً من متوسط القيم المتوقعة لمثبِّتات الدائرة (stabilizers). وسنتبع الخطوات التالية:

  1. إنشاء دائرة: دائرة كليفورد عشوائية ضحلة بنمط الطوب أحادية البعد تُستخدم لتصوير الفحوصات.

  2. تحديد الكيوبتات كأهداف للفحوصات: ربط الدائرة بالكيوبتات الفيزيائية على QPU من نوع Heron المسمى ibm_boston وتحديد الكيوبتات التي لها كيوبتات مساعدة مجاورة.

  3. تحويل الدائرة إلى الواجهة الخلفية (Backend): اختيار تخطيط للكيوبتات وترجمة بوابات الدائرة إلى مجموعة البوابات الأصلية لـ ibm_boston.

  4. إنشاء نموذج ضوضاء من بيانات الـ Backend: توليد نموذج ضوضاء تقريبي بالاعتماد على بيانات المعايرة المرجعية للـ Backend. ستُستخدم بيانات الضوضاء هذه لتقييم فعالية كل فحص إضافي.

  5. تصوير فحوصات باولي: إضافة بعض فحوصات باولي إلى الدائرة الضحلة، حتى نتمكن من تصوير كيفية إضافة الفحوصات.

  6. تحديد المثبِّتات المراد قياسها: زيادة عمق الدائرة لزيادة تأثير ضوضاء البوابات. ثم أخذ عينة عشوائية من مجموعة جزئية من مثبِّتات الدائرة الأعمق.

  7. إيجاد فحوصات جيدة للحالة بأكملها: إيجاد مجموعة واحدة من الفحوصات لمثبِّتات الحالة التي تحضّرها الدائرة. هذه الفحوصات صالحة مهما كان ما يُقاس بعد ذلك، لذا تخدم الدائرة المفحوصة نفسها كل مثبِّت تم أخذ عينة منه.

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

  9. إبقاء عينات الدائرة المفحوصة التي لم تكتشف أخطاء (postselect): إزالة العينات التي تفشل في فحص واحد على الأقل. ثم حساب القيم المتوقعة للدائرة المشوشة العارية والدائرة التي تحتوي على الفحوصات.

  10. مقارنة دقة الحالات المشوشة الأصلية وبعد الانتقاء: إظهار أن إبقاء العينات التي لم تُكتشف فيها أخطاء فقط يحسّن الدقة المقدّرة للحالة المشوشة.

1. إنشاء الدائرة​

أولًا، سننشئ دائرة كليفورد ضحلة تكون بوابات التشابك فيها مرتّبة باتصال بنمط الطوب. سنستخدم هذه الدائرة الصغيرة لتصوير كيفية إضافة الفحوصات إلى الدائرة.

# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util

_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_aer": "qiskit-aer", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "qiskit_paulice": "qiskit-paulice", "samplomatic": "samplomatic"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import numpy as np
from qiskit import QuantumCircuit

def random_clifford_circuit(
num_qubits: int, depth: int, rng: np.random.Generator
) -> QuantumCircuit:
"""Brickwork random Clifford on `num_qubits`, with `depth` CZ layers."""
qc = QuantumCircuit(num_qubits)
qc.h(range(num_qubits))
for d in range(depth):
for i in range(d % 2, num_qubits - 1, 2):
qc.cz(i, i + 1)
for q in range(num_qubits):
if rng.integers(0, 2):
qc.sx(q)
if rng.integers(0, 2):
qc.s(q)
if rng.integers(0, 2):
qc.sx(q)
return qc

num_qubits = 12
depth = 4
seed = 1764
rng = np.random.default_rng(seed)
np.random.seed(seed)
circuit = random_clifford_circuit(num_qubits, depth, rng)
circuit.draw("mpl", fold=-1, scale=0.6)

Output of the previous code cell

2. تحديد الكيوبتات كأهداف للفحوصات​

بعد ذلك نختار تخطيطًا أحادي البعد للكيوبتات على QPU من نوع Heron r3 المسمى ibm_boston. نختار تخطيطًا يتعرج قطريًا عبر مخطط الاقتران ويعظّم عدد الكيوبتات المساعدة المتاحة. ولإيجاد الكيوبتات المساعدة المتاحة نستخدم الدالة get_check_qubits، التي تعيد قوائم من أزواج كيوبت هدف/كيوبت مساعد. سيُنفَّذ فحص باولي على target_qubits[i] باستخدام ancilla_qubits[i].

في مخطط الاقتران أدناه، الكيوبتات الخضراء هي كيوبتات الحمولة، والكيوبتات البرتقالية هي الكيوبتات المساعدة التي سنستخدمها لتنفيذ الفحوصات. الكيوبتات 68 و69 و89 و91 و111 و113 و133 لها كيوبتات مساعدة مجاورة، لذا سنستخدم هذه الكيوبتات (المقابلة للكيوبتات الافتراضية 0 و1 و3 و5 و7 و9 و11) ككيوبتات هدف لتنفيذ الفحوصات. لاحظ أن الكيوبت 134 مجاور للكيوبت 133 في الحمولة لكنه غير مميّز ككيوبت مساعد. والسبب أن 133 اختير مسبقًا (بشكل اعتباطي) لتنفيذ فحص مع الكيوبت المساعد 132.

from qiskit.visualization import plot_coupling_map
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_paulice.layout import get_check_qubits

service = QiskitRuntimeService()
backend = service.backend("ibm_boston")

# Choose a layout and pair each available target qubit with a neighboring ancilla
layout = [68, 69, 78, 89, 90, 91, 98, 111, 112, 113, 119, 133]
target_qubits, ancilla_qubits = get_check_qubits(backend, layout)

print(f"Target qubits: {target_qubits}")
print(f"Ancilla qubits: {ancilla_qubits}")
plot_coupling_map(
num_qubits=backend.num_qubits,
qubit_coordinates=getattr(
backend.configuration(), "qubit_coordinates", None
),
coupling_map=backend.configuration().coupling_map,
figsize=(12, 12),
qubit_color=[
"#4CAF50"
if i in set(layout)
else "#FF9800"
if i in set(ancilla_qubits)
else "#DDDDDD"
for i in backend.coupling_map.graph.node_indices()
],
qubit_size=220,
line_width=2,
font_size=90,
)
Target qubits: [68, 69, 89, 91, 111, 113, 133]
Ancilla qubits: [67, 70, 88, 92, 110, 114, 132]

Output of the previous code cell

3. تحويل الدائرة إلى الواجهة الخلفية​

بما أن الـ Backend قد اختير بالفعل، يمكننا المتابعة لتحويل دائرتنا الافتراضية إلى دائرة ISA. هنا نكتفي بضبط تخطيط الكيوبتات وترجمة البوابات إلى مجموعة البوابات الأصلية لـ ibm_boston.

from qiskit.transpiler import generate_preset_pass_manager

pm = generate_preset_pass_manager(
optimization_level=0, backend=backend, initial_layout=layout
)
circuit_isa = pm.run(circuit)
circuit_isa.draw("mpl", fold=-1, scale=0.6)

Output of the previous code cell

4. إنشاء نموذج ضوضاء من بيانات الـ Backend​

الآن وبعد أن عرفنا كيف ستُوزَّع الدائرة على QPU، يمكننا نمذجة كيفية تأثير ضوضاء البوابات والقراءة في الـ Backend على تنفيذ الدائرة. يُستخدم نموذج الضوضاء لتحديد الأماكن المثلى في الدائرة لوضع فحوصات باولي. نريد وضع الفحوصات في المواضع التي تلتقط فيها أكبر قدر من الخطأ. ورغم أن نموذج ضوضاء أدق يمكن أن يحسّن قدرات الفحوصات على الكشف، فإنه عادةً ليس من الضروري تعلّم نموذج ضوضاء عن طريق أخذ عينات من QPU. هنا نستنتج نموذج ضوضاء بوابات موحّد لإزالة الاستقطاب باستخدام بيانات المعايرة المرجعية للـ Backend فقط من qiskit-ibm-runtime، ونُسقط خطأ القراءة الذي تُبلغ عنه أيضًا: الفحوصات التي سنجدها في الخطوة 7 تقرأ كيوبتها المساعد فقط، لذا تكون أخطاء قراءة الحمولة غير مرئية لها ولن تفعل سوى خفض الدقة بعد الانتقاء في الخطوة 10. راجع وثائق qiskit_paulice.NoiseModel لمزيد من المعلومات حول تحديد ضوضاء البوابات والقراءة. وللمستخدمين الذين يرغبون في استخدام نموذج ضوضاء باولي-ليندبلاد (Pauli-Lindblad) متعلَّم، راجعوا NoiseModel.from_pauli_lindblad_maps.

from dataclasses import replace

from qiskit_paulice.noise_models import NoiseModel

# Model the gate noise as a uniform depolarizing channel based on backend benchmark data
noise_model = replace(
NoiseModel.from_backend(backend, layout, uniform_gate_noise=True),
readout_noise=None,
)
print(noise_model)
NoiseModel(gate_noise=0.0015808382794757693, readout_noise=None, idling_noise=None)

5. تصوير فحوصات باولي​

لنضف الآن بعض الفحوصات إلى الدائرة ونرَ كيف تبدو. يمكننا استخدام الدالة qiskit_paulice.add_pauli_checks لإيجاد مجموعة جيدة من الفحوصات الصالحة. نقدّم دائرة كليفورد الحمولة كمدخل، مع قائمة بكيوبتات الحمولة التي ستُضاف عليها الفحوصات (الكيوبتات الهدف) ونموذج الضوضاء الذي أنشأناه في الخطوة السابقة. في هذا المثال سنضيف فحوصات إلى جميع الكيوبتات الهدف الـ 7 التي لها كيوبتات مساعدة متاحة. تُضاف الفحوصات إلى الكيوبتات بالترتيب الذي تظهر به في target_qubits، مما يسهّل التفكير في التخطيط النهائي للدائرة المفحوصة: final_layout = layout + ancilla_qubits. وإذا قرر المستخدم أنه يفضّل تشغيل إحدى دوائر المخرجات بعدد أقل (i) من الفحوصات، فسيكون التخطيط النهائي final_layout = layout + ancilla_qubits[:i+1].

يوضح تصوير الدائرة أن الفحوصات نُفّذت بشكل صحيح باستخدام أزواج الهدف/المساعد المحددة.

تفاصيل حول qiskit_paulice.add_pauli_checks:

  • يجب أن يكون المدخل الأول دائرة كليفورد. هنا لا تحتوي على قياسات ونمرّر stabilizers="all"، لذا تُوجد الفحوصات لمثبِّتات الحالة التي تحضّرها الدائرة: يكون الفحص صالحًا إذا كان حاصل ضرب باولي الزمكاني الخاص به يقيس مثبِّتًا كهذا، وتكون متلازمته هي بت الكيوبت المساعد وحده، ويبقى صالحًا مهما كان الأساس الذي تُقاس به الحمولة بعد ذلك. وهذا ما يحتاجه تقدير الدقة، إذ يُقاس كل مثبِّت تم أخذ عينة منه في أساس مختلف. (أما حذف stabilizers فيوجد فحوصات لقياسات الدائرة النهائية.)
  • الوسيط الموضعي الثاني هو قائمة بفهارس الكيوبتات الهدف. سيشترك كل كيوبت هدف في بوابات تشابك مع كيوبت مساعد لتشكيل فحص. ستُوجد الفحوصات وتُضاف إلى الدائرة بالترتيب الذي أُعطيت به في هذا الوسيط.
  • الوسيط الموضعي الأخير هو نموذج ضوضاء يمثّل الضوضاء المتوقع أن تؤثر على الدائرة أثناء التنفيذ. يُستخدم هذا النموذج لتقييم فعالية فحص معيّن.
  • ancilla_qubits: إذا كان للدائرة المدخلة تخطيط، يوجّه هذا الوسيط المسمّى add_pauli_checks إلى كيفية إسناد الكيوبتات الفيزيائية إلى الكيوبتات المساعدة المضافة حديثًا. يجب أن يكون ancilla_qubits بنفس طول target_qubits (الوسيط الموضعي الثاني)، ويجب استخدام ancilla_qubits[i] لتنفيذ الفحوصات على target_qubits[i].
  • cost: دالة التكلفة المستخدمة لتقييم الفحوصات. يمكن أن تكون gamma أو LER (معدل الخطأ المنطقي)، وهما طريقتان لوصف مقدار قناة الضوضاء الذي لم تكتشفه الفحوصات. ومع stabilizers="all" يُعدّ الخطأ منطقيًا إذا غيّر الحالة المحضّرة، وهذا بالضبط ما تقيسه الدقة.
  • method: الطريقة المستخدمة لإيجاد الفحوصات الصالحة. نستخدم windowed التي تأخذ عينات عشوائية من العديد من المجموعات الجزئية للنوافذ المكانية لدعم الكيوبت الهدف (الأسلاك). والقيم الصالحة الأخرى هي genetic وwindowed_genetic.
  • يحتوي مخرج add_pauli_checks على دوائر بأعداد متزايدة من الفحوصات، من دائرة بلا فحوصات إلى دائرة بفحوصات على كل كيوبت هدف (بافتراض إيجاد فحوصات صالحة).

راجع الأقسام II-IV من المعلومات التكميلية للحصول على معلومات مفصّلة حول إيجاد فحوصات باولي زمكانية جيدة

import matplotlib.pyplot as plt
from qiskit_paulice import add_pauli_checks

# Find checks on circuit
checked = add_pauli_checks(
circuit_isa,
target_qubits,
noise_model,
ancilla_qubits=ancilla_qubits,
cost="gamma",
method="windowed",
seed=seed,
stabilizers="all",
)

print(f"Physical layout of payload+ancillas: {layout + ancilla_qubits}")
print("Checked circuit:")
checked[-1].circuit.draw("mpl", fold=-1, idle_wires=False)
Physical layout of payload+ancillas: [68, 69, 78, 89, 90, 91, 98, 111, 112, 113, 119, 133, 67, 70, 88, 92, 110, 114, 132]
Checked circuit:

Output of the previous code cell

6. تحديد المثبِّتات المراد قياسها​

لاختبار أداء كشف الأخطاء، سنقدّر دقة الحالة المثبَّتة، ∣ψ⟩=U∣0⟩⊗n|\psi\rangle = U|0\rangle^{\otimes n}، التي تحضّرها الدائرة مثاليًا مقابل الحالة المشوشة، ρ\rho، التي يخرجها العتاد فعليًا. يُعرَّف المسقِط على الحالة المثبَّتة النقية، ∣ψ⟩|\psi\rangle، بأنه المتوسط المنتظم على عناصر 2n2^n من مجموعة المثبِّتات الخاصة بها S\mathcal{S}:

∣ψ⟩⟨ψ∣=12n∑G∈SG|\psi\rangle\langle\psi| = \frac{1}{2^n}\sum_{G \in \mathcal{S}} G

بتعويض ذلك في معادلة الدقة، نرى أن دقة الحالة المشوشة، ρ\rho، تساوي متوسط القيمة المتوقعة لجميع المثبِّتات، G∈SG \in \mathcal{S}، بالنسبة إلى ρ\rho:

F=Tr(ρ∣ψ⟩⟨ψ∣)=Tr(12n∑G∈SρG)=12n∑G∈STr(ρG)=12n∑G∈S⟨G⟩ρF = \mathrm{Tr}(\rho|\psi\rangle\langle\psi|) = \mathrm{Tr}({\frac{1}{2^n}\sum_{G \in \mathcal{S}} \rho G}) = \frac{1}{2^n} \sum_{G \in \mathcal{S}} \mathrm{Tr}(\rho G) = \frac{1}{2^n} \sum_{G \in \mathcal{S}} \langle G \rangle_\rho

في المسائل الأكبر، يكون قياس (أو حتى تعداد) جميع المثبِّتات 2n2^n غير ممكن، لذا تُقدَّر الدقة غالبًا بقياس عينة عشوائية من المثبِّتات. أخذ MM من المثبِّتات G1,…,GMG_1, \ldots, G_M بشكل عشوائي منتظم من S\mathcal{S} يعطي مقدِّرًا غير متحيز للدقة، F^M\hat F_M،

F^M=1M∑i=1M⟨Gi⟩ρ\hat F_M = \frac{1}{M} \sum_{i=1}^M \langle G_i \rangle_\rho في هذه التجربة سنجعل الدائرة أعمق بكثير لتعزيز تأثير ضوضاء البوابات، وسنضيف فحوصات لكل كيوبت له كيوبت مساعد مجاور. وبينما توجد طرق أكثر تطورًا لأخذ عينات من المثبِّتات لتقدير الدقة، سنأخذ في هذا المثال 100100 مثبِّت باحتمال منتظم.

from qiskit.quantum_info import Clifford, Pauli, PauliList

num_stabilizers = 100
num_shots = 1_000
num_checks = len(target_qubits)
depth = 72

circuit = random_clifford_circuit(num_qubits, depth, rng)
# Create all 2^N stabilizers
stabilizers = PauliList([Pauli("I" * num_qubits)])
for generator in (
Pauli(label) for label in Clifford(circuit).to_labels(mode="S")
):
stabilizers = stabilizers + stabilizers.compose(generator)

# Choose some random stabilizers from the set
keep = np.where(stabilizers.x.any(axis=1) | stabilizers.z.any(axis=1))[0]
chosen = np.random.default_rng(seed).choice(
keep, size=min(num_stabilizers, len(keep)), replace=False
)
stabilizers = stabilizers[chosen]
depth = circuit.depth(lambda x: x.operation.num_qubits == 2)
print(
f"Sampled {len(stabilizers)} stabilizers of {circuit.num_qubits}-qubit circuit with two-qubit depth of {depth}: {{{stabilizers[0]}, {stabilizers[1]}, ...}}"
)
Sampled 100 stabilizers of 12-qubit circuit with two-qubit depth of 72: {-YIIYZZZIZIYY, -XIXYIIZXYIZX, ...}

7. إيجاد فحوصات جيدة للحالة بأكملها​

نوجد مجموعة واحدة من الفحوصات للدائرة العميقة تمامًا كما في الخطوة 5. وبما أن الفحوصات تقرأ كيوباتها المساعدة فقط، تخدم الدائرة المفحوصة نفسها كل مثبِّت تم أخذ عينة منه. الدوائر المُعادة تقيس الكيوبتات المساعدة للفحوصات فقط، لذا سنضيف قياسات الحمولة في الخطوة التالية.

import time

# Transpile the deep payload without measurements and find one set of checks for the whole state
payload_isa = pm.run(circuit)
t0 = time.time()
checked = add_pauli_checks(
payload_isa,
target_qubits,
noise_model,
ancilla_qubits=ancilla_qubits,
cost="gamma",
method="windowed",
seed=seed,
stabilizers="all",
)
print(
f"Added {len(checked[-1].check_qubits)} checks in {time.time() - t0:.0f}s."
)
print(
f"2-qubit depth increased from {payload_isa.depth(lambda x: len(x.qubits) == 2)} to "
f"{checked[-1].circuit.depth(lambda x: len(x.qubits) == 2)} when adding {num_checks} checks."
)
Added 7 checks in 3s.
2-qubit depth increased from 72 to 85 when adding 7 checks.

8. أخذ عينات من الدائرة المشوشة​

نأخذ عينات من كل نسخة، من الحمولة العارية إلى الدائرة التي تحتوي على جميع الفحوصات الـ 7، لكل مثبِّت تم أخذ عينة منه. النسخة ذات صفر فحوصات هي الحمولة العارية بنفس القياسات، لذا تعمل كمرجع مشوّش. وبدلًا من إلحاق دوران الأساس لكل مثبِّت بنسخة من كل نسخة، نترك samplomatic يختار أساس القياس وقت أخذ العينات. وفي كل نسخة نقوم بما يلي:

  • قياس الحمولة وتجميع القياسات النهائية في صندوق باستخدام مدير تمريرات التصنيد (boxing pass manager) الخاص بـ samplomatic. يحمل الصندوق تعليقًا توضيحيًا ChangeBasis، لذا تعيد build دائرة قالب تكون بواباتها المغيِّرة للأساس معلَّمة، مع samplex يحسب المعاملات للأساس المطلوب.

  • إضافة القالب والـ samplex إلى QuantumProgram. يأخذ الـ samplex أساس قياس واحدًا لكل كيوبت في الصندوق، بترتيب فيزيائي تصاعدي، مرمَّزًا بـ I=0I=0 وZ=1Z=1 وX=2X=2 وY=3Y=3. وتمرير مصفوفة من الأسس بمحور رئيسي بطول 100 يجعل عنصرًا واحدًا يقيس جميع المثبِّتات الـ 100: الحمولة في أساس كل مثبِّت والكيوبتات المساعدة في ZZ. كما يُقاس أي كيوبت خارج دعم المثبِّت في ZZ، ويُتجاهل بته عند حساب القيمة المتوقعة للمثبِّت.

تشغّل الأولية Executor من qiskit-ibm-runtime البرنامج بأكمله في مهمة واحدة. وعند إعطائها AerSimulator بدلًا من QPU فإنها تعمل محليًا على ذلك المحاكي، لذا نمرّر لها محاكيًا بنفس ضوضاء إزالة الاستقطاب للبوابات التي استخدمناها لإيجاد الفحوصات، ونستخدم طريقة المثبِّتات لأن جميع الدوائر من نوع كليفورد. نحتاج Qiskit Aer فقط لبناء هذا المحاكي: يأخذ الوضع المحلي البذرة من خيارات Executor لكنه يتجاهل noise_model فيها، لذا يجب ضبط الضوضاء على المحاكي نفسه. أما تمرير Backend من نوع QPU بدلًا من ذلك فسيشغّل البرنامج نفسه على العتاد. وبعد اكتمال المحاكاة، سنحسب معدل الانتقاء والقيم المتوقعة لنكوّن فكرة عن مدى نجاح كشف الأخطاء قبل تصوير البيانات.

from qiskit import ClassicalRegister
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel as AerNoiseModel
from qiskit_aer.noise import depolarizing_error
from qiskit_ibm_runtime import Executor
from qiskit_ibm_runtime.quantum_program import QuantumProgram
from samplomatic.builders import build
from samplomatic.transpiler import generate_boxing_pass_manager

# Group the terminal measurements into a box whose measurement basis is chosen at sampling time
boxing_pm = generate_boxing_pass_manager(
enable_gates=False, measure_annotations="change_basis"
)

# bases[i, q]: basis in which physical qubit q is measured for stabilizer i (I=0, Z=1, X=2, Y=3)
bases = np.ones((len(stabilizers), backend.num_qubits), dtype=np.uint8)
bases[:, layout] = stabilizers.z + 2 * stabilizers.x

program = QuantumProgram(shots=num_shots)
for variant in checked:
circ = variant.circuit.copy()
circ.add_register(meas := ClassicalRegister(num_qubits, "meas"))
circ.measure(layout, meas)
template, samplex = build(boxing_pm.run(circ))
(basis_input,) = samplex.inputs().get_specs()
measured_qubits = sorted(layout + list(variant.check_qubits))
program.append_samplex_item(
template,
samplex=samplex,
samplex_arguments={basis_input.name: bases[:, measured_qubits]},
shape=(len(stabilizers),),
)

# Simulate the program with the same gate noise we used to find the checks
aer_nm = AerNoiseModel()
aer_nm.add_all_qubit_quantum_error(
depolarizing_error(noise_model.gate_noise, 2), ["cz"]
)
noisy_sim = AerSimulator(method="stabilizer", noise_model=aer_nm)

# outs[k][register]: bits of the variant with k checks, shape (stabilizers, shots, bits)
t0 = time.time()
outs = (
Executor(noisy_sim, options={"simulator": {"seed_simulator": seed}})
.run(program)
.result()
)
print(
f"Sampled {len(checked)} circuits for {len(stabilizers)} stabilizers in {time.time() - t0:.0f}s."
)
Sampled 8 circuits for 100 stabilizers in 77s.

9. إبقاء عينات الدائرة المفحوصة (postselect)​

في هذه الحزمة يُنفَّذ الفحص باستخدام بوابات تشابك بين كيوبت مساعد واحد وكيوبت هدف واحد. يبدأ كل كيوبت مساعد في ∣0⟩|0\rangle، لذا يثبّت ZancZ_\text{anc} حالته المدخلة. ونشر ZancZ_\text{anc} إلى الأمام من بداية الكيوبت المساعد عبر الدائرة المفحوصة بأكملها يعطي ZancZ_\text{anc} مضروبًا في مثبِّت للحالة المحضّرة، لذا ينجح الفحص إذا كان بت الكيوبت المساعد 00. وتُحتفظ بالعينة إذا نجحت جميع الفحوصات.

تقيس الدوائر المفحوصة الكيوبتات المساعدة إلى السجل checks_c، لذا تُحتفظ بالعينة إذا كان كل بت في ذلك السجل 0. وبشكل عام، تقوم الدالة get_postselection_method في نسخ CheckedCircuit الناتجة من add_pauli_checks بربط سلسلة بتات اللقطة بمتلازمة كل فحص دفعة واحدة؛ وبالنسبة للفحوصات التي أُوجدت لمثبِّتات الحالة المحضّرة، تكون كل متلازمة هي بت كيوبتها المساعد فقط، لذا نقرأ السجل مباشرةً.

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

def keep_mask(out):
"""Per-shot True/False: every ancilla read 0, so every Pauli check passed."""
if "checks_c" not in out: # the bare payload has no checks
return np.ones(out["meas"].shape[:-1], dtype=bool)
return ~out["checks_c"].any(axis=-1)

# masks[k][i]: kept shots of stabilizer i measured on the variant with k checks
masks = [keep_mask(out) for out in outs]
psrs_arr = np.array([mask.mean(axis=-1) for mask in masks]).T
ks = np.arange(psrs_arr.shape[1])

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(ks, psrs_arr.T, color="darkorange", alpha=0.15, linewidth=1)
ax.plot(
ks,
np.median(psrs_arr, axis=0),
color="black",
linewidth=1,
linestyle="--",
label="median",
)
ax.set(
xlabel="Checks committed",
ylabel="Post-selection rate",
ylim=(0, 1.05),
title=f"Per-stabilizer post-selection rate ({len(psrs_arr)} stabilizers)",
)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Output of the previous code cell

10. مقارنة دقة الحالات المشوشة الأصلية وبعد الانتقاء​

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

def expectation_values(out, mask):
"""EV of each stabilizer from the shots of ``out`` kept by ``mask``.

Bit q of ``meas`` is virtual qubit q, so a stabilizer's eigenvalue in a shot is the parity of
the bits in its support, times its sign.
"""
support = stabilizers.x | stabilizers.z
parity = (out["meas"] & support[:, None, :]).sum(axis=-1) % 2
signs = np.where(stabilizers.phase == 2, -1.0, 1.0)
eigenvalues = np.where(parity, -1.0, 1.0) * signs[:, None]
kept = mask.sum(axis=-1)
return np.where(
kept > 0,
(eigenvalues * mask).sum(axis=-1) / np.maximum(kept, 1),
np.nan,
)

evs_noisy = expectation_values(outs[0], masks[0])
evs_post = expectation_values(outs[-1], masks[-1])
print(
f"ideal fidelity: 1.0\n"
f"noisy fidelity: {np.nanmean(evs_noisy):.4f}\n"
f"postselected fidelity: {np.nanmean(evs_post):.4f}\n"
f"mean post-sel rate: {masks[-1].mean():.3f}\n"
)

evs_ideal = np.ones(len(stabilizers))
idx = np.arange(len(stabilizers))

def cum_mean_sem(ys):
valid = ~np.isnan(ys)
s = np.cumsum(np.where(valid, ys, 0.0))
s2 = np.cumsum(np.where(valid, ys**2, 0.0))
n = np.maximum(np.cumsum(valid).astype(float), 1)
mean = s / n
sem = np.sqrt(np.maximum(s2 / n - mean**2, 0) / n)
return np.where(np.cumsum(valid) > 0, mean, np.nan), sem

def strip(ax, ys, color, label):
m, s = np.nanmean(ys), np.nanstd(ys)
ax.axhspan(
m - s, m + s, color=color, alpha=0.15, label=f"{label} mean ± std"
)
ax.axhline(m, color=color, linewidth=1, linestyle="--")

fig, ax = plt.subplots(figsize=(8, 4))
ax.axhline(np.nanmean(evs_ideal), color="black", linewidth=1.5, label="ideal")
strip(ax, evs_noisy, "red", "noisy")
strip(ax, evs_post, "green", "post-sel")
ax.scatter(
idx, evs_noisy, color="red", s=22, alpha=0.7, zorder=3, label="noisy <G>"
)
ax.scatter(
idx,
evs_post,
color="green",
s=22,
alpha=0.7,
zorder=3,
label="post-sel <G>",
)
ax.set(
xlabel="stabilizer index",
ylabel=r"$\langle G \rangle$",
ylim=(-0.1, 1.1),
title="Per-stabilizer EVs",
)
ax.legend(loc="lower left", ncol=2)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

M = np.arange(1, len(stabilizers) + 1)
fig, ax = plt.subplots(figsize=(8, 4))
for ys, color, label in [
(evs_ideal, "black", "ideal"),
(evs_noisy, "red", "noisy"),
(evs_post, "green", "post-sel"),
]:
cm, sem = cum_mean_sem(ys)
ax.plot(M, cm, color=color, linewidth=1.5, label=label)
ax.fill_between(M, cm - sem, cm + sem, color=color, alpha=0.15)
ax.set(
xlabel="number of stabilizers averaged",
ylabel="running fidelity estimate",
title="Fidelity convergence vs. number of stabilizers",
)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
ideal fidelity: 1.0
noisy fidelity: 0.5582
postselected fidelity: 0.9747
mean post-sel rate: 0.535

Output of the previous code cell

Output of the previous code cell

بما أن مجموعة واحدة من الفحوصات تخدم كل مثبِّت، فهناك منحنى غاما واحد فقط للنظر إليه: قيمة دالة التكلفة بعد اعتماد كل فحص. ولمسائل أخرى على مجموعة مختلفة من الكيوبتات الفيزيائية، ومع نموذج ضوضاء غير مثالي (تذكّر أن نموذج الضوضاء في هذا الدرس نمذج بدقة الضوضاء المؤثرة على العينات)، يمكن أن يكون لترتيب إضافة الفحوصات إلى الكيوبتات الهدف تأثير أوضح على كيفية تقارب المنحنى.

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

costs = [variant.cost for variant in checked]

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(range(len(costs)), costs, color="steelblue", marker="o")
ax.set(
xlabel="Checks committed",
ylabel="Gamma",
yscale="log",
title="Gamma of the noise left uncovered by the checks",
)
ax.grid(True, alpha=0.3, which="both")
plt.tight_layout()
plt.show()

Output of the previous code cell