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

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

تقدير الاستخدام: 4 دقائق على معالج Heron (ibm_kingston أو ما يعادله) (ملاحظة: هذا تقدير فقط. قد يختلف وقت التشغيل لديك.)

مخرجات التعلم

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

  • كيفية استخدام حزمة qiskit-paulice للعثور على فحوصات فعالة من حيث العتاد وإدراجها تلقائياً باستخدام get_check_qubits وNoiseModel وadd_pauli_checks.

  • كيفية تقدير دقة حالة مثبِّتة عبر أخذ عينات من مثبِّتاتها والاختيار اللاحق بناءً على متلازمات الفحص.

  • كيفية تشغيل سير عمل كشف الأخطاء الكامل على عتاد IBM Quantum® ومقارنة الدقات المشوَّشة والمختارة لاحقاً.

المتطلبات المسبقة

  • أساسيات العتاد للحوسبة الكمومية على المقياس النفعي.

  • صورية كليفورد والمثبِّت الشكلية، بما في ذلك كيفية وصف مجموعة مثبِّتة لحالة مثبِّتة نقية.

الخلفية

كشف أخطاء منخفض الحمل الزائد باستخدام رموز المكان-الزمان [1] بقلم Simon Martiel وAli Javadi-Abhari يُقدّم طريقة لكشف الأخطاء المنطقية في الدوائر ذات الهيمنة الكليفوردية تقع بين التصحيح الكامل للأخطاء والتخفيف الأخف وزناً للأخطاء. تبني الفكرة على فحوصات باولي المتماسكة (CPC) من التخفيف من الأخطاء بلقطة واحدة عبر فحوصات باولي المتماسكة [2] بقلم van den Berg وآخرين. في كلا النهجين، تُشبَّك دائرة "حمولة" كليفوردية مع كيوبتات مساعدة للتحقق من ثوابت معينة. يُنتج قياس الكيوبتات المساعدة متلازمة تُبلّغ عمّا إذا كان قد اكتُشف خطأ أثناء التنفيذ. الاحتفاظ فقط بالعينات التي لم يُكتشف فيها خطأ يُحسّن دقة التوزيع المُعاين، بتكلفة معدل اختيار لاحق مُخفَّض.

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

ما تفعله حزمة qiskit-paulice

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

  • get_check_qubits يفحص خريطة اقتران الخلفية ويُعيد أزواج كيوبتات الهدف والمساعدة. يستخدم فحص على target_qubits[i] الكيوبت ancilla_qubits[i].

  • NoiseModel.from_backend يبني نموذج ضجيج تقريبياً من بيانات القياس المرجعي للخلفية. يُقيّم النموذج الفحوصات المرشحة، لذا لا يُطلب نموذج ضجيج دقيق ومُتعلَّم. لنموذج باولي-ليندبلاد مُتعلَّم، راجع NoiseModel.from_pauli_lindblad_maps.

  • add_pauli_checks يجد ويُدرج الفحوصات في دائرة. يُعيد سلسلة من كائنات CheckedCircuit بعدد متزايد من الفحوصات، ويوفّر كل كائن get_postselection_method يُحوّل سلسلة بت مقاسة إلى متجه متلازمة. تختار وسيطة cost الدالة التي تُقيّم فحصاً (gamma، حمل زائد أخذ العينات لقناة الضجيج المعكوسة المُختارة لاحقاً، أو LER، معدل الخطأ المنطقي). تختار وسيطة method استراتيجية البحث (windowed أو genetic أو windowed_genetic). يستخدم هذا الدليل التعليمي cost="gamma" وmethod="windowed"، اللتان تعطيان معاً اختياراً محدداً وقابلاً لإعادة الإنتاج للفحوصات.

تقدير الدقة من أخذ عينات المثبِّت

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

ψψ=12nGSG.|\psi\rangle\langle\psi| = \frac{1}{2^n}\sum_{G \in \mathcal{S}} G.

بتعويض هذا في الدقة، نحصل على دقة ρ\rho كمتوسط قيمة توقع كل مثبِّت GSG \in \mathcal{S} بالنسبة إلى ρ\rho:

F=Tr(ρψψ)=12nGSTr(ρG)=12nGSGρ.F = \mathrm{Tr}(\rho|\psi\rangle\langle\psi|) = \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 غير عملي، لذا يمكنك تقدير الإخلاص (fidelity) من عينة عشوائية. سحب MM من المثبتات G1,,GMG_1, \ldots, G_M بشكل عشوائي منتظم من S\mathcal{S} يعطي تقديراً غير متحيز:

F^M=1Mi=1MGiρ.\hat F_M = \frac{1}{M} \sum_{i=1}^M \langle G_i \rangle_\rho.

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

المتطلبات

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

  • حزمة تطوير Qiskit SDK الإصدار 2.0 أو أحدث، مع دعم التصور

  • Qiskit Runtime الإصدار 0.40 أو أحدث (pip install qiskit-ibm-runtime)

  • Qiskit Aer الإصدار 0.17 أو أحدث (pip install qiskit-aer)

  • Qiskit Paulice (pip install qiskit-paulice)

  • tqdm (pip install tqdm)

الإعداد

استورد المكتبات المطلوبة وعرّف الدوال المساعدة غير المتوفرة كاستيرادات. تبني الدالة random_clifford_circuit حمولة كليفورد عشوائية بنمط الطوب (brickwork)، وتبحث find_check_layout في خريطة اقتران الخلفية (backend) عن مسار كيوبت منخفض الخطأ يحتوي على العديد من الأنسلّة (ancillas) المتاحة، وتحوّل learned_noise_model مخرجات NoiseLearner إلى نموذج ضجيج من qiskit-paulice، وتُدوّر append_basis_rotation دارة بحيث يُقاس المثبت في الأساس الحسابي، وتحسب expectation قيمة توقع مثبت من العدّات المأخوذة عينياً، وتتتبع cum_mean_sem تقدير الإخلاص التراكمي.

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime qiskit-paulice tqdm
# Standard library imports
import random
import time

# External libraries
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm

# Qiskit
from qiskit import QuantumCircuit
from qiskit.quantum_info import Clifford, Pauli, PauliLindbladMap, PauliList
from qiskit.result import sampled_expectation_value
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.visualization import plot_coupling_map

# Qiskit Aer
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel as AerNoiseModel
from qiskit_aer.noise import ReadoutError, depolarizing_error

# Qiskit IBM Runtime
from qiskit_ibm_runtime import NoiseLearner, QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2 as Sampler

# Qiskit Paulice
from qiskit_paulice import add_pauli_checks
from qiskit_paulice.layout import get_check_qubits
from qiskit_paulice.noise_models import NoiseModel
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

def find_check_layout(
backend,
num_qubits: int,
rng: np.random.Generator,
num_trials: int = 200,
max_gate_error: float = 0.03,
max_readout_error: float = 0.2,
) -> list[int]:
"""Find a low-error path of `num_qubits` qubits with many available ancillas.

Builds random self-avoiding walks on the coupling map, excluding the qubits
and two-qubit gates whose reported errors exceed the thresholds, and keeps
the path that offers the most target and ancilla pairs. Ties are broken by
the lower average two-qubit gate error along the path.
"""
target = backend.target
gate_2q = next(
name for name in ("cz", "ecr", "cx") if name in target.operation_names
)

# Collect per-edge gate errors and per-qubit readout errors
edge_error = {}
for qubits, props in target[gate_2q].items():
edge = tuple(sorted(qubits))
if props is not None and props.error is not None:
edge_error[edge] = min(edge_error.get(edge, 1.0), props.error)
readout_error = {
qubit: target["measure"][(qubit,)].error
for (qubit,) in target["measure"]
}

# Keep only the edges whose gate and readout errors are acceptable
adjacency = {}
for (q1, q2), error in edge_error.items():
if (
error <= max_gate_error
and readout_error.get(q1, 1.0) <= max_readout_error
and readout_error.get(q2, 1.0) <= max_readout_error
):
adjacency.setdefault(q1, set()).add(q2)
adjacency.setdefault(q2, set()).add(q1)

# Random self-avoiding walks; keep the path with the most check pairs
starts = sorted(adjacency)
best_path = None
best_score = (-1, float("inf"))
for _ in range(num_trials):
path = [starts[rng.integers(len(starts))]]
while len(path) < num_qubits:
options = sorted(adjacency[path[-1]] - set(path))
if not options:
break
path.append(options[rng.integers(len(options))])
if len(path) < num_qubits:
continue
num_pairs = len(get_check_qubits(backend.coupling_map, path)[0])
mean_error = float(
np.mean(
[edge_error[tuple(sorted(e))] for e in zip(path, path[1:])]
)
)
if num_pairs > best_score[0] or (
num_pairs == best_score[0] and mean_error < best_score[1]
):
best_path, best_score = path, (num_pairs, mean_error)

if best_path is None:
raise RuntimeError(
"No connected low-error path found. Relax the error thresholds."
)
return best_path

def learned_noise_model(layer_errors, layout: list[int]) -> NoiseModel:
"""Build a `NoiseModel` from `NoiseLearner` results.

`NoiseLearner` reports one `PauliLindbladError` per entangling layer, whose
generators are indexed against that layer's own physical qubits, while
`NoiseModel.from_pauli_lindblad_maps` expects `PauliLindbladMap`s indexed the
way `NoiseModel.from_backend` indexes them: by position in `layout`. This
translates between the two and drops generators that fall outside `layout`.
"""
phys_to_virt = {phys: virt for virt, phys in enumerate(layout)}
maps = []
for layer in layer_errors:
if layer.error is None:
continue
terms = []
for pauli, rate in zip(
layer.error.generators, layer.error.rates, strict=True
):
label, indices = [], []
for local, phys in enumerate(layer.qubits):
x, z = bool(pauli.x[local]), bool(pauli.z[local])
if not (x or z):
continue
if phys not in phys_to_virt:
break # generator reaches outside the layout, so skip it
label.append("Y" if x and z else "X" if x else "Z")
indices.append(phys_to_virt[phys])
else:
if label:
terms.append(
("".join(label), tuple(indices), float(rate))
)
# Each map needs a 2-qubit generator to define an entangling layer
if any(len(t[1]) == 2 for t in terms):
maps.append(
PauliLindbladMap.from_sparse_list(
terms, num_qubits=len(layout)
)
)
if not maps:
raise RuntimeError(
"No usable layer errors. Check that the learner ran on this layout."
)
return NoiseModel.from_pauli_lindblad_maps(maps)

def append_basis_rotation(
circuit: QuantumCircuit, pauli: Pauli
) -> QuantumCircuit:
"""Strip measurements, append basis rotations for `pauli`, and re-measure."""
out = circuit.remove_final_measurements(inplace=False)
for q in range(pauli.num_qubits):
if pauli.x[q]:
if pauli.z[q]:
out.sdg(q)
out.h(q)
out.measure_all()
return out

def expectation(counts: dict, pauli: Pauli) -> float:
"""Expectation value of `pauli` from counts measured in the Z basis.

Pads with identity on any qubits beyond the support of `pauli`, such as the
check ancillas that appear in the postselected counts.
"""
if not counts:
return float("nan")
n = pauli.num_qubits
sign = -1 if int(pauli.phase) % 4 == 2 else 1
total = len(next(iter(counts)))
label = "".join(
"Z" if q < n and (pauli.x[q] or pauli.z[q]) else "I"
for q in range(total - 1, -1, -1)
)
return sign * sampled_expectation_value(counts, label)

def cum_mean_sem(values: np.ndarray):
"""Cumulative mean and standard error of the mean, ignoring NaNs."""
valid = ~np.isnan(values)
total = np.cumsum(np.where(valid, values, 0.0))
total_sq = np.cumsum(np.where(valid, values**2, 0.0))
count = np.maximum(np.cumsum(valid).astype(float), 1)
mean = total / count
sem = np.sqrt(np.maximum(total_sq / count - mean**2, 0) / count)
return np.where(np.cumsum(valid) > 0, mean, np.nan), sem

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

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

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

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

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.measure_all()
circuit.draw("mpl", fold=-1, scale=0.6)

Output of the previous code cell

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

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

أولاً، اختر خلفية (backend) وابحث في خريطة اقترانها عن تخطيط كيوبتات أحادي البعد باستخدام المساعد find_check_layout المعرّف في قسم الإعداد. يبني المساعد مسارات عشوائية ذاتية التجنب تتجنب أعلى البوابات وعمليات القراءة خطأً، ويحتفظ بالمسار الذي يوفر أكبر عدد من أزواج الأهداف والأنسلّة. لأن البحث يقرأ بيانات الاتصال والخطأ من الخلفية نفسها، يعمل نفس الكود على أي وحدة معالجة كمومية QPU من IBM Quantum. ثم تُرجع الدالة get_check_qubits أزواج الأهداف والأنسلّة، حيث يستخدم الفحص على target_qubits[i] الأنسلة ancilla_qubits[i].

في رسم بيانية الاقتران التالي، الكيوبتات الخضراء هي كيوبتات الحمولة والكيوبتات البرتقالية هي الأنسلّة التي تُنفّذ الفحوصات. تُستخدم الكيوبتات التي بجوارها أنسلة كأهداف للفحوصات.

service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)

print(f"Backend: {backend.name}")

# Search for a low-error path, then pair each target qubit with a neighboring ancilla
layout = find_check_layout(backend, num_qubits, rng)
target_qubits, ancilla_qubits = get_check_qubits(backend, layout)
num_checks = len(target_qubits)

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,
)
Backend: ibm_boston
Target qubits: [105, 107, 108, 123, 125, 141, 143]
Ancilla qubits: [104, 97, 109, 122, 126, 140, 144]

Output of the previous code cell

بعد اختيار الخلفية والتخطيط، حوّل الحمولة إلى دارة معمارية مجموعة تعليمات (ISA). لا يلزم سوى ضبط التخطيط وترجمة البوابات إلى مجموعة البوابات الأصلية للخلفية.

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

بعد ذلك، نمذج كيفية تأثير ضجيج البوابات والقراءة على الخلفية في التنفيذ. يحدد نموذج الضجيج أين في الدارة يلتقط الفحص أكبر قدر من الخطأ. يحسّن النموذج الأدق الاكتشاف، لكن ليس من الضروري عادةً تعلّم نموذج بأخذ عينات من الـ QPU. النموذج التالي يستنتج قناة إزالة استقطاب موحدة لضجيج البوابات والقراءة من بيانات قياس أداء qiskit-ibm-runtime.

noise_model = NoiseModel.from_backend(
backend, layout, uniform_gate_noise=True
)
print(noise_model)
NoiseModel(gate_noise=0.001079865281450939, readout_noise=0.006001790364583333, idling_noise=None)

الآن أضف فحوصات إلى الدارة. تأخذ الدالة add_pauli_checks حمولة كليفورد، وقائمة كيوبتات الهدف، ونموذج الضجيج. تخبر الوسيطة ancilla_qubits الدالة بأي أنسلة فيزيائية يجب اقترانها بكل هدف. تُضاف الفحوصات بالترتيب الذي تظهر فيه كيوبتات الهدف، لذا يكون التخطيط النهائي للدارة المفحوصة هو layout + ancilla_qubits. لتشغيل دارة مخرجة بعدد أقل (i) من الفحوصات، يكون التخطيط النهائي layout + ancilla_qubits[:i].

مخرجات add_pauli_checks هي سلسلة من الدارات بعدد متزايد من الفحوصات، من دون فحوصات إلى فحص واحد على كل كيوبت هدف. يؤكد التصور أن الفحوصات تستخدم أزواج الأهداف والأنسلّة المحددة. للاطلاع على تفاصيل إيجاد فحوصات جيدة، راجع الأقسام II إلى IV من المعلومات التكميلية في المرجع [1].

checked = add_pauli_checks(
circuit_isa,
target_qubits,
noise_model,
ancilla_qubits=ancilla_qubits,
cost="gamma",
method="windowed",
seed=seed,
)

print(f"Physical layout of payload and ancillas: {layout + ancilla_qubits}")
print("Checked circuit:")
checked[-1].circuit.draw("mpl", fold=-1, idle_wires=False)
Physical layout of payload and ancillas: [108, 107, 106, 105, 117, 125, 124, 123, 136, 143, 142, 141, 104, 97, 109, 122, 126, 140, 144]
Checked circuit:

Output of the previous code cell

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

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

ابنِ الدارة الأعمق واسحب عينة عشوائية من مثبتاتها.

depth = 24
num_stabilizers = 20
num_shots = 1_000

circuit = random_clifford_circuit(num_qubits, depth, rng)

# Build the full stabilizer group, then sample from it uniformly at random
circ_no_meas = circuit.remove_final_measurements(inplace=False)
stabilizer_group = PauliList([Pauli("I" * num_qubits)])
for generator in (
Pauli(label) for label in Clifford(circ_no_meas).to_labels(mode="S")
):
stabilizer_group = stabilizer_group + stabilizer_group.compose(generator)

keep = np.where(
stabilizer_group.x.any(axis=1) | stabilizer_group.z.any(axis=1)
)[0]
chosen = np.random.default_rng(seed).choice(
keep, size=min(num_stabilizers, len(keep)), replace=False
)
stabilizers = [stabilizer_group[int(i)] for i in chosen]

two_qubit_depth = circuit.depth(lambda x: x.operation.num_qubits == 2)
print(
f"Sampled {len(stabilizers)} stabilizers of a {circuit.num_qubits}-qubit "
f"circuit with two-qubit depth {two_qubit_depth}: "
f"{{{stabilizers[0]}, {stabilizers[1]}, ...}}"
)
Sampled 20 stabilizers of a 12-qubit circuit with two-qubit depth 24: {ZXIIXZYYXIZZ, XXXYIIZYXIII, ...}

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

noisy_circuits = []
checked_circuits = []
depths_2q = []
t0 = time.time()
for i, pauli in enumerate(tqdm(stabilizers)):
noisy_circuits.append(pm.run(append_basis_rotation(circuit, pauli)))
# Shuffle target and ancilla pairs together so each target keeps its ancilla
targets, ancillas = zip(
*random.sample(
list(zip(target_qubits, ancilla_qubits, strict=True)),
k=len(target_qubits),
),
strict=True,
)
checked_circuits.append(
add_pauli_checks(
noisy_circuits[-1],
list(targets),
noise_model,
ancilla_qubits=list(ancillas),
cost="gamma",
method="windowed",
seed=seed + 1 + i,
)
)
depths_2q.append(
checked_circuits[-1][-1].circuit.depth(lambda x: len(x.qubits) == 2)
)

print(
f"Added {num_checks} checks to {len(stabilizers)} circuits "
f"in {(time.time() - t0):.0f}s."
)
print(
f"On average, two-qubit depth increased from "
f"{circuit.depth(lambda x: len(x.qubits) == 2)} to {int(np.mean(depths_2q))} "
f"when adding {num_checks} checks."
)
100%|██████████| 20/20 [00:15<00:00, 1.29it/s]
Added 7 checks to 20 circuits in 15s.
On average, two-qubit depth increased from 24 to 33 when adding 7 checks.

خذ عينة من الحمولة العارية والدارات المفحوصة باستخدام Qiskit Aer. يستخدم المحاكي نفس نموذج إزالة الاستقطاب الذي قيّم الفحوصات، لذا فإن الضجيج الذي تستهدفه الفحوصات هو الضجيج الذي يطبقه المحاكي.

aer_nm = AerNoiseModel()
aer_nm.add_all_qubit_quantum_error(
depolarizing_error(noise_model.gate_noise, 2), ["cz"]
)
p = noise_model.readout_noise
aer_nm.add_all_qubit_readout_error(ReadoutError([[1 - p, p], [p, 1 - p]]))
noisy_sim = AerSimulator(method="stabilizer", noise_model=aer_nm)

counts = []
for i, checked_circ_result in enumerate(tqdm(checked_circuits)):
noisy_counts = (
noisy_sim.run(
noisy_circuits[i], shots=num_shots, seed_simulator=seed * i + 1
)
.result()
.get_counts()
)
checked_counts_per_variant = []
for k, ck in enumerate(checked_circ_result):
variant_counts = (
noisy_sim.run(
ck.circuit, shots=num_shots, seed_simulator=seed * i + 2 + k
)
.result()
.get_counts()
)
checked_counts_per_variant.append(variant_counts)
counts.append((noisy_counts, checked_counts_per_variant))
100%|██████████| 20/20 [00:17<00:00, 1.13it/s]

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

يستخدم كل فحص بوابات تشابك بين أنسلة واحدة وهدف واحد. تبدأ الأنسلة في 0|0\rangle، لذا ZancZ_\text{anc} يثبّت مدخلها. نشر ZancZ_\text{anc} إلى الأمام عبر الدارة المفحوصة يعطي مؤثر باولي على المخرجات تحدد حدوده غير المطابقة للهوية دعم الفحص. ينجح الفحص عندما يكون للبتات في دعمه تكافؤ زوجي. تُحتفظ بالعينة فقط عندما ينجح كل فحص.

تُرجع get_postselection_method لكل CheckedCircuit دالة تُعيّن سلسلة بتات مقيسة إلى متجه متلازمة (syndrome). احتفظ بالعينات التي تكون متلازمتها صفراً لكل فحص، وتخلص من الباقي. يُظهر الرسم البياني التالي أن إضافة المزيد من الفحوصات يخفض معدل الانتقاء اللاحق. يحتاج معدل الانتقاء اللاحق الأقل إلى المزيد من اللقطات (shots) للوصول إلى دقة مستهدفة، لذا هناك مقايضة بين قدرة الاكتشاف وتكلفة أخذ العينات. يبدو أن المعدل يتقارب، مما يشير إلى أن الفحوصات الإضافية تسهم بقدرة اكتشاف أقل.

rate_per_variant = []
kept_per_stab = []
for i, (_, checked_counts_per_variant) in enumerate(counts):
rates = []
kept_at_num_checks = None
for k, variant_counts in enumerate(checked_counts_per_variant):
ps_fn = checked_circuits[i][k].get_postselection_method()
kept = {
bs: n for bs, n in variant_counts.items() if not ps_fn(bs).any()
}
rates.append(sum(kept.values()) / num_shots)
if k == num_checks:
kept_at_num_checks = kept
rate_per_variant.append(rates)
kept_per_stab.append(kept_at_num_checks)

max_len = max(len(s) for s in rate_per_variant)
rates_arr = np.full((len(rate_per_variant), max_len), np.nan)
for i, s in enumerate(rate_per_variant):
rates_arr[i, : len(s)] = s
ks = np.arange(max_len)

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(ks, rates_arr.T, color="#ff8c00", alpha=0.15, linewidth=1)
ax.plot(
ks,
np.nanmedian(rates_arr, axis=0),
color="black",
linewidth=1,
linestyle="--",
label="median",
)
ax.set_xlabel("Checks committed")
ax.set_ylabel("Postselection rate")
ax.set_ylim((0, 1.05))
ax.set_title(
f"Per-stabilizer postselection rate ({len(rates_arr)} stabilizers)"
)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Output of the previous code cell

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

results = []
for i, ((noisy_counts, _), kept) in enumerate(
zip(counts, kept_per_stab, strict=True)
):
results.append(
(
expectation(noisy_counts, stabilizers[i]),
expectation(kept, stabilizers[i]),
sum(kept.values()) / num_shots,
)
)

fidelity_noisy = float(np.nanmean([r[0] for r in results]))
fidelity_postsel = float(np.nanmean([r[1] for r in results]))
psr = float(np.mean([r[2] for r in results]))
print(
f"ideal fidelity: 1.0\n"
f"noisy fidelity: {fidelity_noisy:.4f}\n"
f"postselected fidelity: {fidelity_postsel:.4f}\n"
f"mean postselection rate: {psr:.3f}"
)

evs_ideal = np.ones(len(results))
evs_noisy = np.array([r[0] for r in results])
evs_post = np.array([r[1] for r in results])
idx = np.arange(len(results))

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 and std"
)
ax.axhline(
m, color=color, linewidth=1, linestyle="--", label=f"{label} fidelity"
)

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", "postselected")
ax.scatter(idx, evs_noisy, color="red", s=22, alpha=0.7, label="noisy EVs")
ax.scatter(
idx,
evs_post,
color="green",
s=22,
alpha=0.7,
label="postselected EVs",
)
ax.set_xlabel("stabilizer index")
ax.set_ylabel(r"$\langle G \rangle$")
ax.set_ylim((-0.1, 1.1))
ax.set_title("Per-stabilizer expectation values")
ax.legend(loc="lower left")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

M = np.arange(1, len(results) + 1)
fig, ax = plt.subplots(figsize=(8, 4))
for ys, color, label in [
(evs_ideal, "black", "ideal"),
(evs_noisy, "red", "noisy"),
(evs_post, "green", "postselected"),
]:
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")
ax.set_ylabel("running fidelity estimate")
ax.set_title("Fidelity convergence versus number of stabilizers")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
ideal fidelity: 1.0
noisy fidelity: 0.7899
postselected fidelity: 0.9679
mean postselection rate: 0.780

Output of the previous code cell

Output of the previous code cell

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

stab_scores = [
[variant.cost for variant in checked_circ_result]
for checked_circ_result in checked_circuits
]
max_len = max(len(s) for s in stab_scores)
scores = np.full((len(stab_scores), max_len), np.nan)
for i, s in enumerate(stab_scores):
scores[i, : len(s)] = s
ks = np.arange(max_len)

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(ks, scores.T, color="#4682b4", alpha=0.15, linewidth=1)
ax.plot(
ks,
np.nanmedian(scores, axis=0),
color="black",
linewidth=1,
linestyle="--",
label="median",
)
ax.set_xlabel("Checks committed")
ax.set_ylabel("Gamma")
ax.set_yscale("log")
ax.set_title(f"Per-stabilizer gamma curves ({len(scores)} stabilizers)")
ax.legend()
ax.grid(True, alpha=0.3, which="both")
plt.tight_layout()
plt.show()

Output of the previous code cell

مثال عتاد كبير النطاق

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

شيء واحد يتغير بالنسبة لمثال المحاكي: بدلاً من استنتاج قناة إزالة استقطاب موحدة من بيانات المعايرة، يتعلم هذا القسم نموذج الضجيج باستخدام NoiseLearner ويبني نموذج qiskit-paulice من النتيجة باستخدام NoiseModel.from_pauli_lindblad_maps. يلتقط نموذج باولي-ليندبلاد المتعلَّم البنية المكانية للضجيج على هذا التخطيط المحدد، بدلاً من افتراض أن كل حافة مشوشة بنفس القدر، لذا يُقيَّم وضع الفحوصات مقابل ضجيج أشبه بذلك المؤثر على الـ QPU. يتطلب تعلّم الضجيج أخذ عينات من الـ QPU وينبغي احتسابه في أي ميزانية إجمالية لأخذ العينات من الـ QPU.

تضبط المعاملات التالية عدد الكيوبتات، والعمق، وعدد المثبتات، وعدد اللقطات. قم بتوسيع hw_num_shots مع مقلوب معدل الانتقاء اللاحق: عند معدل 3%، تترك 40,000 لقطة ما يقارب 1,200 عينة منتقاة لاحقاً لكل دارة. زد hw_num_stabilizers لتقدير إخلاص أدق مقابل زيادة عدد الدارات لكل وظيفة، كل منها يحتاج نفس ميزانية اللقطات.

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

تُشغّل الخلية التالية نفس الخطوات الأربع كما في مثال المحاكي. تبني الحمولة الأكبر وتأخذ عينة من عدد قليل من المثبتات (الخطوة 1)؛ وتختار تخطيطاً، وتتعلم نموذج الضجيج عليه، وتجد الدارة المفحوصة بالكامل لكل مثبت (الخطوة 2)؛ وتقدّم وظيفة Sampler واحدة تحتوي على الدارات العارية والمفحوصة معاً (الخطوة 3)؛ وتُجري الانتقاء اللاحق على العدّات المفحوصة لمقارنة تقديرات الإخلاص المشوش وبعد الانتقاء اللاحق، لكل مثبت وفي المتوسط (الخطوة 4). عند هذا الحجم، يصبح تعداد مجموعة المثبتات الكاملة كما في مثال المحاكي غير عملي، لذا تأخذ الخلية عينة فرعية عشوائية من المثبتات لحساب تقدير للإخلاص.

لاحظ أن الخطوة 2 تفعل هنا أكثر مما في مثال المحاكي: تعلّم نموذج الضجيج يقدم وظيفة NoiseLearner خاصة به قبل وظيفة Sampler، لذا تُشغّل الخلية وظيفتين بالمجموع. تحمل الوسمين TUT_ASPC_LEARN وTUT_ASPC حتى تتمكن من إيجادهما لاحقاً. راجع تنظيم والبحث عبر وسوم الوظائف لمزيد حول وسم الوظائف.

# -------------------------Step 1: build a larger payload and sample stabilizers-------------------------
hw_num_qubits = 20
hw_depth = 36
hw_num_stabilizers = 10
hw_num_shots = 40_000

hw_circuit = random_clifford_circuit(hw_num_qubits, hw_depth, rng)
hw_no_meas = hw_circuit.remove_final_measurements(inplace=False)

# Enumerating all 2^n stabilizers is infeasible at this size, so draw each
# stabilizer by composing a random subset of the group generators
hw_generators = [
Pauli(label) for label in Clifford(hw_no_meas).to_labels(mode="S")
]
sample_rng = np.random.default_rng(seed)
hw_stabilizers = []
while len(hw_stabilizers) < hw_num_stabilizers:
mask = sample_rng.integers(0, 2, hw_num_qubits).astype(bool)
if not mask.any():
continue # skip the identity
stabilizer = Pauli("I" * hw_num_qubits)
for generator, chosen in zip(hw_generators, mask, strict=True):
if chosen:
stabilizer = stabilizer.compose(generator)
hw_stabilizers.append(stabilizer)

# -------------------------Step 2: find a 20-qubit layout, learn its noise, and add checks-------------------------
# A single bad coupler or bad-readout qubit on the path drags every
# stabilizer down, so search harder and with tighter error thresholds
hw_layout = find_check_layout(
backend,
hw_num_qubits,
rng,
num_trials=500,
max_gate_error=0.015,
max_readout_error=0.05,
)
hw_target_qubits, hw_ancilla_qubits = get_check_qubits(backend, hw_layout)
hw_pm = generate_preset_pass_manager(
optimization_level=0, backend=backend, initial_layout=hw_layout
)
print(f"Layout with {len(hw_target_qubits)} check pairs: {hw_layout}")

# ----- learn a Pauli-Lindblad noise model on this layout -----
# The simulator example scored checks against a uniform depolarizing channel
# inferred from calibration data. Here, learn the noise instead: NoiseLearner
# runs its own job on the QPU and returns a Pauli-Lindblad channel per unique
# entangling layer, so the checks are placed against the noise this layout
# actually has, including its spatial structure. All the sampled stabilizers
# share the same entangling layers and differ only in their final basis
# rotation, so learning on the bare payload covers all of them.
learner = NoiseLearner(
mode=backend,
options={
"max_layers_to_learn": 4,
"num_randomizations": 32,
"shots_per_randomization": 128,
"environment": {"job_tags": ["TUT_ASPC_LEARN"]},
},
)
learner_job = learner.run([hw_pm.run(hw_circuit)])
print(f"Submitted noise-learner job {learner_job.job_id()}")
hw_layer_errors = learner_job.result().data

# To see how much the learned model helps, swap the next line for the
# simulator example's uniform model - a one-line change:
# hw_noise_model = NoiseModel.from_backend(backend, hw_layout, uniform_gate_noise=True)
hw_noise_model = learned_noise_model(hw_layer_errors, hw_layout)
# NoiseLearner characterizes gate noise only, so keep the readout estimate
# from calibration data rather than leaving it unset
hw_noise_model.readout_noise = NoiseModel.from_backend(
backend, hw_layout, uniform_gate_noise=True
).readout_noise
print(
f"Learned {len(hw_layer_errors)} layers; "
f"readout noise {hw_noise_model.readout_noise:.5f}"
)

# ----- add the fully checked circuit per stabilizer -----
hw_noisy_circuits = []
hw_checked_circuits = []
for i, pauli in enumerate(tqdm(hw_stabilizers)):
bare = hw_pm.run(append_basis_rotation(hw_circuit, pauli))
hw_noisy_circuits.append(bare)
variants = add_pauli_checks(
bare,
hw_target_qubits,
hw_noise_model,
ancilla_qubits=hw_ancilla_qubits,
cost="gamma",
method="windowed",
seed=seed + 1 + i,
)
hw_checked_circuits.append(variants[-1]) # keep the fully checked circuit

# -------------------------Step 3: submit one Sampler job with the bare and checked circuits-------------------------
sampler = Sampler(mode=backend)
sampler.options.default_shots = hw_num_shots
sampler.options.environment.job_tags = ["TUT_ASPC"]

pubs = hw_noisy_circuits + [cc.circuit for cc in hw_checked_circuits]
job = sampler.run(pubs)
print(f"Submitted job {job.job_id()} with {len(pubs)} circuits")

# -------------------------Step 4: postselect and compare fidelity-------------------------
result = job.result()
n_stab = len(hw_stabilizers)

hw_results = []
for i in range(n_stab):
noisy_counts = result[i].join_data().get_counts()
checked_counts = result[n_stab + i].join_data().get_counts()
ps_fn = hw_checked_circuits[i].get_postselection_method()
kept = {bs: c for bs, c in checked_counts.items() if not ps_fn(bs).any()}
hw_results.append(
(
expectation(noisy_counts, hw_stabilizers[i]),
expectation(kept, hw_stabilizers[i]),
sum(kept.values()) / sum(checked_counts.values()),
)
)

hw_fidelity_noisy = float(np.nanmean([r[0] for r in hw_results]))
hw_fidelity_postsel = float(np.nanmean([r[1] for r in hw_results]))
hw_psr = float(np.mean([r[2] for r in hw_results]))
print(
f"noisy fidelity estimate: {hw_fidelity_noisy:.4f}\n"
f"postselected fidelity estimate: {hw_fidelity_postsel:.4f}\n"
f"mean postselection rate: {hw_psr:.4f} "
f"(~{int(round(hw_psr * hw_num_shots))} kept shots per circuit)"
)

# Per-stabilizer breakdown. The postselection rate varies from stabilizer to
# stabilizer, so a stabilizer whose postselected value barely moves is usually
# one whose checks rejected little; the kept-shot count says how much of the
# gap is statistics rather than signal.
print("\nper-stabilizer results:")
print(
f"{'idx':>3} {'noisy':>8} {'postsel':>8} {'psr':>7} {'kept shots':>10}"
)
for i, (noisy, post, psr_i) in enumerate(hw_results):
print(
f"{i:>3} {noisy:>8.4f} {post:>8.4f} {psr_i:>7.4f} "
f"{int(round(psr_i * hw_num_shots)):>10}"
)

hw_noisy = np.array([r[0] for r in hw_results])
hw_post = np.array([r[1] for r in hw_results])
idx = np.arange(n_stab)

fig, ax = plt.subplots(figsize=(9, 4))
ax.axhline(1.0, color="black", linewidth=1.5, label="ideal")
strip(ax, hw_noisy, "red", "noisy")
strip(ax, hw_post, "green", "postselected")
ax.scatter(idx, hw_noisy, color="red", s=22, alpha=0.7, label="noisy EVs")
ax.scatter(
idx,
hw_post,
color="green",
s=22,
alpha=0.7,
label="postselected EVs",
)
ax.set_xlabel("stabilizer index")
ax.set_ylabel(r"$\langle G \rangle$")
ax.set_ylim((-0.1, 1.1))
ax.set_xticks(idx)
ax.set_title("Per-stabilizer expectation values on hardware")
# Outside the axes so it cannot hide a data point
ax.legend(loc="center left", bbox_to_anchor=(1.02, 0.5), frameon=False)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Layout with 11 check pairs: [153, 152, 151, 138, 131, 130, 129, 118, 109, 110, 111, 98, 91, 90, 89, 78, 69, 70, 71, 58]
Submitted noise-learner job d9f4mncjeosc73fjfmkg
Learned 4 layers; readout noise 0.00470
100%|██████████| 10/10 [01:01<00:00, 6.18s/it]
Submitted job d9f4s04jeosc73fjftkg with 20 circuits
noisy fidelity estimate: 0.3685
postselected fidelity estimate: 0.6869
mean postselection rate: 0.2851 (~11404 kept shots per circuit)

per-stabilizer results:
idx noisy postsel psr kept shots
0 0.3769 0.6918 0.3247 12987
1 0.3745 0.6760 0.2999 11995
2 0.3659 0.6389 0.3549 14196
3 0.3821 0.7060 0.2660 10641
4 0.3653 0.7475 0.2531 10124
5 0.3752 0.7022 0.2698 10791
6 0.3508 0.7144 0.2711 10842
7 0.3485 0.7087 0.2381 9523
8 0.3825 0.6289 0.2928 11711
9 0.3630 0.6549 0.2808 11232

Output of the previous code cell

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

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

توصيات

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

المراجع

  • [1] Martiel, S., & Javadi-Abhari, A. (2025). Low-overhead error detection with spacetime codes. arXiv preprint arXiv:2504.15725.

  • [2] van den Berg, E., Bravyi, S., Gambetta, J. M., Jurcevic, P., Maslov, D., & Temme, K. (2023). Single-shot error mitigation by coherent Pauli checks. Physical Review Research, 5(3), 033193.