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

الانتقاء اللاحق مع فحوصات الأخطاء غير الماركوفية

يشرح هذا الدليل ما هي فحوصات الأخطاء غير الماركوفية وكيفية تنفيذها برمجيًا باستخدام qiskit-mitigation. ستبدأ بمثال صغير من كيوبتين لتصوّر الفحوصات ومناقشة كيفية عملها. وبعد أن تكوّن بعض الحدس، ستجرّب مثالًا من 20 كيوبت تنفّذ فيه الفحوصات وتقارن استراتيجيات انتقاء لاحق متنوعة.

ما هي فحوصات الأخطاء غير الماركوفية؟​

فحوصات الأخطاء غير الماركوفية هي اختبارات قائمة على القياس لمعرفة ما إذا كانت حالة الكيوبت مرجّحة التلف بسبب مصدر خطأ غير ماركوفي. يُجرى هذا الاختبار بوضع بوابة قلب بت ذات مدة ممتدة (xslow أو سلسلة من NN بوابات rx بزاوية πN\frac{\pi}{N}) على كل كيوبت في بداية الدائرة، يتبعها XGate عادية، ثم القياس للتأكد من أن الحالة انقلبت بنجاح ∣0⟩↦∣1⟩↦∣0⟩|0\rangle \mapsto |1\rangle \mapsto |0\rangle. ويمكن أيضًا إجراء هذا الاختبار في نهاية الدائرة بعد كل قياس نهائي للتأكد من أن الجهاز قادر على قلب الحالة المقيسة للكيوبت بنجاح من ∣x⟩↦∣x⊕1⟩|x\rangle \mapsto |x\oplus1\rangle. قد تُعدّ العينات المرتبطة بفحوصات فاشلة غير موثوقة، ويمكن أن يؤدي انتقاء العينات التي تجتاز جميع الفحوصات فقط إلى رفع دقة التوزيع المضوضَّ.

استراتيجيات الانتقاء اللاحق بناءً على فحوصات الأخطاء غير الماركوفية​

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

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

كيوبتات المراقبة​

يمكن تحديد كيوبتات مساعدة "مراقِبة" (spectator) مجاورة للكيوبتات المستخدمة لتنفيذ الدائرة الأصلية. يمكن وضع فحوصات الأخطاء غير الماركوفية على هذه الكيوبتات المراقِبة، مما يوفّر تغطية أكبر ضد الضوضاء غير الماركوفية التي صُممت هذه الفحوصات لكشفها.

الجزء 1. تصوّر فحوصات الأخطاء غير الماركوفية​

قبل عرض التقنية على QPU، ستنشئ دائرة صغيرة من كيوبتين وتضيف إليها فحوصات لتصوّر كيفية عملها. ابدأ بحالة Bell مترجمة (transpiled) إلى QPU الخاص بالـ backend ibm_kingston.

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

_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "qiskit_mitigation": "qiskit-mitigation", "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")
from qiskit import QuantumCircuit
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService(channel="ibm_quantum_platform")
backend = service.backend("ibm_kingston")

qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
qc_isa = generate_preset_pass_manager(
optimization_level=0, backend=backend
).run(qc)
qc_isa.draw("mpl")

Output of the previous code cell

أضف بعض فحوصات الأخطاء غير الماركوفية إلى بداية الدائرة. ستوضع الفحوصات فقط على الكيوبتات التي تنتهي بقياس. تضمن هذه الفحوصات أن الجهاز قادر على قلب الحالة الابتدائية للكيوبت بنجاح من ∣0⟩↦∣1⟩↦∣0⟩|0\rangle \mapsto |1\rangle \mapsto |0\rangle، وتُقرأ قياسات هذه الفحوصات في سجل كلاسيكي جديد اسمه meas_pre.

from qiskit.transpiler import PassManager
from qiskit_mitigation.postselection.passes import (
AddPreCircuitNonMarkovianErrorChecks,
)

qc_w_checks = PassManager([AddPreCircuitNonMarkovianErrorChecks()]).run(
qc_isa
)
qc_w_checks.draw("mpl", idle_wires=False)

Output of the previous code cell

وبالمثل، يمكنك إضافة فحوصات إلى نهاية الدائرة بعد كل قياس نهائي. تضمن هذه الفحوصات أن الجهاز قادر على قلب حالة الكيوبت من ∣x⟩↦∣x⊕1⟩|x\rangle \mapsto |x\oplus1\rangle، وتُقرأ قياساتها في سجل كلاسيكي جديد اسمه meas_ps.

from qiskit_mitigation.postselection.passes import (
AddPostCircuitNonMarkovianErrorChecks,
)

qc_w_checks = PassManager([AddPostCircuitNonMarkovianErrorChecks()]).run(
qc_w_checks
)
qc_w_checks.draw("mpl", idle_wires=False)

Output of the previous code cell

كما ذكرنا في المقدمة، تقدر تستخدم كيوبتات مساعدة (ancilla) مجاورة لكيوبتات البيانات كـ "مراقبين" عشان توفر تغطية أكبر للأخطاء. أضف فحوصات مراقبة قبل الدائرة وبعدها. تقدر تشوف إن الكيوبت الفيزيائي 2 على ibm_kingston مجاور لكيوبت البيانات 1، فتمت إضافته كمراقب، وأُضيفت له فحوصات قبل الدائرة وبعدها. أُضيفت قياسات المراقبين إلى سجلات كلاسيكية جديدة اسمها spec_pre و spec.

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

from qiskit_mitigation.postselection.passes import (
AddSpectatorPostCircuitNonMarkovianErrorChecks,
AddSpectatorPreCircuitNonMarkovianErrorChecks,
)

qc_w_checks = PassManager(
[
AddSpectatorPreCircuitNonMarkovianErrorChecks(
coupling_map=backend.coupling_map
),
AddSpectatorPostCircuitNonMarkovianErrorChecks(
coupling_map=backend.coupling_map
),
]
).run(qc_w_checks)
qc_w_checks.draw("mpl", idle_wires=False, fold=-1)

Output of the previous code cell

الجزء 2: عرض فحوصات الأخطاء غير الماركوفية على QPU ومقارنة استراتيجيات الانتقاء اللاحق​

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

في هذي التجربة نحسب القيم المتوقعة لمُرصَد (observable). اختيار المُرصَد OO مو مهم هو كمان، فنختار ببساطة متوسط المغنطة أحادية الموقع O=1N∑i⟨Zi⟩O=\frac{1}{N}\sum_i\langle Z_i\rangle.

import numpy as np
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import CouplingMap

num_qubits = 20
initial_layout = initial_layout = [
124,
125,
126,
127,
128,
129,
130,
131,
132,
133,
134,
135,
139,
155,
154,
153,
152,
151,
150,
149,
][:num_qubits]
coupling_map = CouplingMap.from_line(num_qubits, bidirectional=False)

# Num Trotter steps and `rx` angle
num_steps = 6
theta_rx = np.pi / 6

# Average single-site magnetization
observable = (
SparsePauliOp(
[
"I" * iq + "Z" + "I" * (num_qubits - iq - 1)
for iq in range(num_qubits)
]
)
/ num_qubits
)

# Create the Trotter circuit
circuit = QuantumCircuit(num_qubits)
edges = CouplingMap.from_line(num_qubits, bidirectional=False).get_edges()
edges_a = [e for e in edges if e[0] % 2 == 0]
edges_b = [e for e in edges if e[0] % 2 == 1]
for _ in range(num_steps):
circuit.rx(theta_rx, [i for i in range(num_qubits)])
for edge in edges_a:
circuit.cz(edge[0], edge[1])
for edge in edges_b:
circuit.cz(edge[0], edge[1])
circuit = circuit.compose(circuit.inverse())
circuit.measure_all()

# Transpile circuit to backend
pm = generate_preset_pass_manager(
optimization_level=0, backend=backend, initial_layout=initial_layout
)
isa_circuit = pm.run(circuit)
print(
f"two-qubit depth: {isa_circuit.depth(lambda instr: instr.operation.num_qubits == 2)}"
)
print(f"total depth: {isa_circuit.depth()}")
isa_circuit.draw("mpl", fold=-1, idle_wires=False)
two-qubit depth: 24
total depth: 85

Output of the previous code cell

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

# Use all non-Markovian error checks so we can compare all strategies in post-processing.
combined = PassManager(
[
AddPreCircuitNonMarkovianErrorChecks(x_pulse_type="rx"),
AddSpectatorPreCircuitNonMarkovianErrorChecks(
backend.coupling_map, x_pulse_type="rx"
),
AddPostCircuitNonMarkovianErrorChecks(x_pulse_type="rx"),
AddSpectatorPostCircuitNonMarkovianErrorChecks(
backend.coupling_map, x_pulse_type="rx"
),
]
).run(isa_circuit)
combined_isa = generate_preset_pass_manager(
optimization_level=0, backend=backend
).run(combined)

print("cregs:", [(c.name, c.size) for c in combined_isa.cregs])
cregs: [('meas', 20), ('meas_pre', 20), ('spec_pre', 7), ('meas_ps', 20), ('spec', 7), ('spec_ps', 7)]

استخدم الأساس البدائي Executor لأخذ عينات من الدائرة اللي تحتوي على الفحوصات.

from qiskit_ibm_runtime import Executor
from samplomatic.quantum_program import QuantumProgram

# Raw and combined-checks circuits in a single 100k-shot program so they share calibration.
program = QuantumProgram(shots=100_000)
program.append_circuit_item(isa_circuit) # raw circuit, no checks
program.append_circuit_item(
combined_isa
) # combined pre + post non-Markovian error checks
job = Executor(backend).run(program)
print(f"job id: {job.job_id()}")
print(f"status: {job.status()}")
job id: d9badrm6hjac73ffe31g
status: QUEUED

الفئة PostSelector تعرف بنية creg الخاصة بالفحوصات، فتقدر تربط كل بت في العينات بقياس الدائرة المرتبط فيه. تقدر تستخدم طريقتها compute_mask لإنشاء قناع على تسلسل من العينات، بحسب استراتيجية الانتقاء اللاحق (عقدية أو حافّية) ونوع فحوصات الأخطاء غير الماركوفية المطلوب النظر فيها (قبل الدائرة أو بعدها). ولدمج استراتيجيات قبل الدائرة وبعدها في قناع واحد، تقدر تحسب القناعين بشكل مستقل ثم تدمجهما بالعملية AND. هنا نستخدم PostSelector لإنشاء أقنعة لكل الاستراتيجيات المختلفة وننظر في النتائج باستخدام كل تبديلات الاستراتيجيات المتاحة.

في التصوّر البصري أدناه نشوف كيف تؤثر استراتيجية الانتقاء اللاحق على معدل الانتقاء وكذلك على جودة الجواب. وكما هو متوقع، الاستراتيجيات اللي تعتمد الانتقاء العقدي أعطت أقل معدلات انتقاء. أما الاستراتيجيات الحافّية فهي أكثر تساهلاً وتميل لإعطاء معدلات انتقاء أعلى. على المستخدمين تجربة الاستراتيجيات المختلفة على مسألتهم لتحديد التوازن المناسب بين معدل الانتقاء وجودة التوزيع المأخوذ منه العينات. في هذا المثال، استخدام الانتقاء العقدي مع فحوصات قبل الدائرة وبعدها أعطى أفضل جواب بمعدل انتقاء .028%. بعض الاستراتيجيات ما تفوّقت على الدائرة الخام. وتقدر تشوف كمان إن إضافة الفحوصات مع عدم استخدام أي منها (العمود المسمى "no selection" في الرسم أدناه) يعطي أسوأ أداء.

import matplotlib.pyplot as plt
from qiskit_mitigation.postselection import PostSelector

selector = PostSelector.from_circuit(combined_isa, backend.coupling_map)

exec_results = job.result()
# The Executor returns numpy arrays keyed by classical-register name, so each result item is
# already the {creg_name: ndarray} mapping that compute_mask expects.
raw_bits = exec_results[0]["meas"] # raw circuit, no checks
bits = exec_results[1] # combined pre+post circuit

meas_bits = bits["meas"]
total = meas_bits[..., 0].size

def magnetization(b, mask=None):
"""Average single-site magnetization over the (optionally masked) measurement bits."""
kept = b if mask is None else b[mask]
return (1 - 2 * kept.astype(int)).mean() # bit 0 -> +1, bit 1 -> -1

rows = []

def report(label, mask=None, b=meas_bits):
"""Record and print the magnetization and kept fraction for one selection strategy."""
kept = b[..., 0].size if mask is None else int(mask.sum())
ev = magnetization(b, mask)
rows.append((label, ev, kept / total))
print(f"{label:<24}: <m> = {ev:+.4f} (kept {kept:>6}/{total})")

# Compare all possible postselection strategies
report("raw circuit", b=raw_bits)
report("no selection")
for s in ("node", "edge"):
report(f"pre-only ({s})", selector.compute_mask(bits, s, mode="pre"))
for s in ("node", "edge"):
report(f"post-only ({s})", selector.compute_mask(bits, s, mode="post"))
for pre_s in ("node", "edge"):
for post_s in ("node", "edge"):
keep = selector.compute_mask(
bits, pre_s, mode="pre"
) & selector.compute_mask(bits, post_s, mode="post")
report(f"pre={pre_s} + post={post_s}", keep)

labels = [r[0] for r in rows]
evs = [r[1] for r in rows]
fracs = [r[2] for r in rows]
x = range(len(labels))

fig, ax1 = plt.subplots(figsize=(11, 5))

# Bars: expectation value per selection strategy.
bar_colors = ["firebrick"] + ["steelblue"] * (len(evs) - 1)
ax1.bar(x, evs, color=bar_colors, alpha=0.85)
ax1.axhline(1.0, color="green", ls=":", lw=1.5, label="exact <m>")
ax1.axhline(rows[0][1], color="firebrick", ls="--", lw=1, label="raw <m>")
ax1.set_ylabel("<m> (avg single-site magnetization)", color="steelblue")
ax1.tick_params(axis="y", labelcolor="steelblue")
ax1.set_xticks(list(x))
ax1.set_xticklabels(labels, rotation=45, ha="right")
ax1.legend(loc="upper right")

# Line: fraction of shots retained (right axis).
ax2 = ax1.twinx()
ax2.plot(x, fracs, "o-", color="darkorange")
ax2.set_ylabel("kept fraction", color="darkorange")
ax2.tick_params(axis="y", labelcolor="darkorange")
ax2.set_ylim(0, 1.05)

ax1.set_title("Selection strategy: expectation value vs shots retained")
fig.tight_layout()
plt.show()
raw circuit : <m> = +0.5791 (kept 100000/100000)
no selection : <m> = +0.5211 (kept 100000/100000)
pre-only (node) : <m> = +0.6026 (kept 1357/100000)
pre-only (edge) : <m> = +0.5637 (kept 13029/100000)
post-only (node) : <m> = +0.5682 (kept 2254/100000)
post-only (edge) : <m> = +0.5265 (kept 27514/100000)
pre=node + post=node : <m> = +0.6393 (kept 28/100000)
pre=node + post=edge : <m> = +0.5799 (kept 442/100000)
pre=edge + post=node : <m> = +0.6006 (kept 317/100000)
pre=edge + post=edge : <m> = +0.5521 (kept 3836/100000)

Output of the previous code cell