שילוב אפשרויות הפחתת שגיאות עם ה-Estimator primitive
הערכת זמן שימוש: שבע דקות על מעבד Heron r2 (הערה: זוהי הערכה בלבד. זמן הריצה שלך עשוי להשתנות.)
רקע
מדריך זה חוקר את אפשרויות דיכוי השגיאות והפחתת השגיאות הזמינות עם ה-Estimator primitive של Qiskit Runtime. תבנו מעגל ו-observable ותגישו משימות באמצעות ה-Estimator primitive תוך שימוש בשילובים שונים של הגדרות הפחתת שגיאות. לאחר מכן, תציגו את התוצאות כדי לצפות בהשפעות של ההגדרות השונות. רוב הדוגמאות משתמשות במעגל בעל 10 קיוביטים כדי להקל על ויזואליזציה, ובסוף תוכלו להגדיל את תהליך העבודה ל-50 קיוביטים.
אלה אפשרויות דיכוי והפחתת השגיאות שבהן תשתמשו:
- Dynamical decoupling
- הפחתת שגיאות מדידה (Measurement error mitigation)
- Gate twirling
- אקסטרפולציית רעש אפס (Zero-noise extrapolation - ZNE)
דרישות
לפני תחילת מדריך זה, ודאו שהדברים הבאים מותקנים:
- Qiskit SDK גרסה 2.1 ומעלה, עם תמיכה ב-visualization
- Qiskit Runtime גרסה 0.40 ומעלה (
pip install qiskit-ibm-runtime)
הגדרה
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-runtime
import matplotlib.pyplot as plt
import numpy as np
from qiskit.circuit.library import efficient_su2, unitary_overlap
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Batch, EstimatorV2 as Estimator
שלב 1: מיפוי קלטים קלאסיים לבעיה קוונטית
מדריך זה מניח שהבעיה הקלאסית כבר מופתה לקוונטית. התחילו בבניית מעגל ו-observable למדידה. בעוד שהטכניקות המשמשות כאן חלות על סוגים רבים ושונים של מעגלים, לשם פשטות מדריך זה משתמש במעגל efficient_su2 הכלול בספריית המעגלים של Qiskit.
efficient_su2 הוא מעגל קוונטי מפרמטר שתוכנן להיות ניתן להרצה יעילה על חומרה קוונטית עם קישוריות קיוביטים מוגבלת, תוך שהוא עדיין מספיק אקספרסיבי כדי לפתור בעיות בתחומי יישום כמו אופטימיזציה וכימיה. הוא בנוי על ידי לסירוגין שכבות של שערי qubit יחיד מפרמטרים עם שכבה המכילה תבנית קבועה של שערי שני קיוביטים, למספר נבחר של חזרות. התבנית של שערי שני הקיוביטים יכולה להיקבע על ידי המשתמש. כאן תוכלו להשתמש בתבנית המובנית pairwise מכיוון שהיא ממזערת את עומק המעגל על ידי אריזת שערי שני הקיוביטים בצפיפות מירבית. תבנית זו יכולה להתבצע באמצעות קישוריות קיוביטים לינארית בלבד.
n_qubits = 10
reps = 1
circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)
circuit.decompose().draw("mpl", scale=0.7)


עבור ה-observable שלנו, ניקח את אופרטור Pauli הפועל על הקיוביט האחרון, .
# Z on the last qubit (index -1) with coefficient 1.0
observable = SparsePauliOp.from_sparse_list(
[("Z", [-1], 1.0)], num_qubits=n_qubits
)
בשלב זה, תוכלו להמשיך להריץ את המעגל שלכם ולמדוד את ה-observable. עם זאת, תרצו גם להשוות את הפלט של המכשיר הקוונטי עם התשובה הנכונה - כלומר, הערך התיאורטי של ה-observable, אם המעגל היה מבוצע ללא שגיאה. עבור מעגלים קוונטיים קטנים אתם יכולים לחשב ערך זה על ידי סימולציה של המעגל על מחשב קלאסי, אבל זה לא אפשרי עבור מעגלים גדולים יותר, בקנה מידה שירותי. תוכלו לעקוף בעיה זו עם טכניקת "מעגל המראה" (ידועה גם בשם "compute-uncompute"), שהיא שימושית לבחינת ביצועים של מכשירים קוונטיים.
מעגל מראה
בטכניקת מעגל המראה, אתם משרשרים את המעגל עם המעגל ההפוך שלו, שנוצר על ידי הפיכת כל שער של המעגל בסדר הפוך. המעגל המתקבל מיישם את אופרטור הזהות, שניתן לסמלץ אותו בטריוויאליות. מכיוון שהמבנה של המעגל המקורי נשמר במעגל המראה, ביצוע מעגל המראה עדיין נותן מושג כיצד המכשיר הקוונטי יבצע את המעגל המקורי.
תא הקוד הבא מקצה פרמטרים אקראיים למעגל שלכם, ולאחר מכן בונה את מעגל המראה באמצעות מחלקת unitary_overlap. לפנ י שיקוף המעגל, הוסיפו הוראת barrier אליו כדי למנוע מה-transpiler למזג את שני חלקי המעגל משני צידי המחסום. ללא המחסום, ה-transpiler ימזג את המעגל המקורי עם ההפוך שלו, וכתוצאה מכך מעגל מתורגם ללא שערים כלל.
# Generate random parameters
rng = np.random.default_rng(1234)
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)
# Assign the parameters to the circuit
assigned_circuit = circuit.assign_parameters(params)
# Add a barrier to prevent circuit optimization of mirrored operators
assigned_circuit.barrier()
# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)
mirror_circuit.decompose().draw("mpl", scale=0.7)


שלב 2: אופטימיזציה של הבעיה להרצה על חומרה קוונטית
עליכם לבצע אופטימיזציה של המעגל שלכם לפני הרצתו על חומרה. תהליך זה כולל מספר שלבים:
- בחירת פריסת קיוביטים שממפה את הקיוביטים הוירטואליים של המעגל שלכם לקיוביטים פיזיים על החומרה.
- הוספת שערי swap לפי הצורך כדי לנתב אינטראקציות בין קיוביטים שאינם מחוברים.
- תרגום השערים במעגל שלכם להוראות Instruction Set Architecture (ISA) שניתן להריץ ישירות על החומרה.
- ביצוע אופטימיזציות מעגל כדי למזער את עומק המעגל וספירת השערים.
ה-transpiler המובנה ב-Qiskit יכול לבצע את כל השלבים הללו עבורכם. מכיוון שדוגמה זו משתמשת במעגל יעיל מבחינת חומרה, ה-transpiler אמור להיות מסוגל לבחור פריסת קיוביטים שאינה דורשת הכנסת שערי swap כלשהם עבור ניתוב אינטראקציות.
עליכם לבחור את מכשיר החומרה לשימוש לפני אופטימיזציה של המעגל שלכם. תא הקוד הבא מבקש את המ כשיר הפחות עסוק עם לפחות 127 קיוביטים.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
תוכלו לתרגם את המעגל שלכם עבור ה-backend שנבחר על ידי יצירת pass manager ולאחר מכן הרצת ה-pass manager על המעגל. דרך קלה ליצור pass manager היא להשתמש בפונקציה generate_preset_pass_manager. ראו Transpile with pass managers להסבר מפורט יותר על תרגום עם pass managers.
pass_manager = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=1234
)
isa_circuit = pass_manager.run(mirror_circuit)
isa_circuit.draw("mpl", idle_wires=False, scale=0.7, fold=-1)


המעגל המתורגם מכיל כעת רק הוראות ISA. שערי ה-qubit היחיד פורקו במונחים של שערי וסיבובי , ושערי ה-CX פורקו ל-שערי ECR וסיבובי qubit יחיד.
תהליך התרגום מיפה את הקיוביטים הוירטואליים של המעגל לקיוביטים פיזיים על החומרה. המידע על פריסת הקיוביטים מאוחסן במאפיין layout של המעגל המתורגם. ה-observable גם הוגדר במונחים של הקיוביטים הוירטואליים, כך שתצטרכו להחיל פריסה זו על ה-observable, דבר שניתן לעשות באמצעות מתודת apply_layout של SparsePauliOp.
isa_observable = observable.apply_layout(isa_circuit.layout)
print("Original observable:")
print(observable)
print()
print("Observable with layout applied:")
print(isa_observable)
Original observable:
SparsePauliOp(['ZIIIIIIIII'],
coeffs=[1.+0.j])
Observable with layout applied:
SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])
שלב 3: הרצה באמצעות Qiskit primitives
אתם כעת מוכנים להריץ את המעגל שלכם באמצעות ה-Estimator primitive.
כאן תגישו חמש משימות נפרדות, החל מללא דיכוי או הפחתת שגיאות, ובאופן עוקב תפעילו אפשרויות דיכוי והפחתת שגיאות שונות הזמינות ב-Qiskit Runtime. למידע על האפשרויות, עיינו בעמודים הבאים:
- סקירה של כל האפשרויות
- Dynamical decoupling
- Resilience, כולל הפחתת שגיאות מדידה ואקסטרפולציית רעש אפס (ZNE)
- Twirling
מכיוון שמשימות אלה יכולות לרוץ באופן עצמאי זו מזו, תוכלו להשתמש ב-batch mode כדי לאפשר ל-Qiskit Runtime לבצע אופטימיזציה של תזמון הביצוע שלהן.
pub = (isa_circuit, isa_observable)
jobs = []
with Batch(backend=backend) as batch:
estimator = Estimator(mode=batch)
# Set number of shots
estimator.options.default_shots = 100_000
# Disable runtime compilation and error mitigation
estimator.options.resilience_level = 0
# Run job with no error mitigation
job0 = estimator.run([pub])
jobs.append(job0)
# Add dynamical decoupling (DD)
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XpXm"
job1 = estimator.run([pub])
jobs.append(job1)
# Add readout error mitigation (DD + TREX)
estimator.options.resilience.measure_mitigation = True
job2 = estimator.run([pub])
jobs.append(job2)
# Add gate twirling (DD + TREX + Gate Twirling)
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"
job3 = estimator.run([pub])
jobs.append(job3)
# Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 3, 5)
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
job4 = estimator.run([pub])
jobs.append(job4)
שלב 4: עיבוד לאחר והחזרת התוצאה בפורמט קלאסי רצוי
לבסוף, תוכלו לנתח את הנתונים. כאן תשלפו את תוצאות המשימות, תחלצו את ערכי התוחלת הנמדדים מהן, ותציגו את הערכים, כולל סרגלי שגיאה של סטיית תקן אחת.
# Retrieve the job results
results = [job.result() for job in jobs]
# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]
# Unpack the expectation values and standard errors
expectation_vals = np.array(
[float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
[float(pub_result.data.stds) for pub_result in pub_results]
)
# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
range(len(labels)),
expectation_vals,
yerr=standard_errors,
label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")
plt.show()
בקנה מידה קטן זה, קשה לראות את ההשפעה של רוב טכניקות הפחתת השגיאות, אבל אקסטרפולציית רעש אפס אכן נותנת שיפור ניכר. עם זאת, שימו לב ששיפור זה אינו מגיע בחינם, מכיוון שלתוצאת ה-ZNE יש גם סרגל שגיאה גדול יותר.
הגדלת הניסוי
בעת פיתוח ניסוי, שימושי להתחיל עם מעגל קטן כדי להקל על ויזואליזציות וסימולציות. כעת, לאחר שפיתחתם ובדקתם את תהליך העבודה שלנו על מעגל בעל 10 קיוביטים, תוכלו להגדיל אותו ל-50 קיוביטים. תא הקוד הבא חוזר על כל השלבים במדריך זה, אך כעת מיישם אותם על מעגל בעל 50 קיוביטים.
n_qubits = 50
reps = 1
# Construct circuit and observable
circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)
observable = SparsePauliOp.from_sparse_list(
[("Z", [-1], 1.0)], num_qubits=n_qubits
)
# Assign parameters to circuit
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)
assigned_circuit = circuit.assign_parameters(params)
assigned_circuit.barrier()
# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)
# Transpile circuit and observable
isa_circuit = pass_manager.run(mirror_circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)
# Run jobs
pub = (isa_circuit, isa_observable)
jobs = []
with Batch(backend=backend) as batch:
estimator = Estimator(mode=batch)
# Set number of shots
estimator.options.default_shots = 100_000
# Disable runtime compilation and error mitigation
estimator.options.resilience_level = 0
# Run job with no error mitigation
job0 = estimator.run([pub])
jobs.append(job0)
# Add dynamical decoupling (DD)
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XpXm"
job1 = estimator.run([pub])
jobs.append(job1)
# Add readout error mitigation (DD + TREX)
estimator.options.resilience.measure_mitigation = True
job2 = estimator.run([pub])
jobs.append(job2)
# Add gate twirling (DD + TREX + Gate Twirling)
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"
job3 = estimator.run([pub])
jobs.append(job3)
# Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 3, 5)
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
job4 = estimator.run([pub])
jobs.append(job4)
# Retrieve the job results
results = [job.result() for job in jobs]
# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]
# Unpack the expectation values and standard errors
expectation_vals = np.array(
[float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
[float(pub_result.data.stds) for pub_result in pub_results]
)
# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
range(len(labels)),
expectation_vals,
yerr=standard_errors,
label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")
plt.show()
כאשר תשוו את תוצאות 50 הקיוביטים עם תוצאות 10 הקיוביטים מקודם, תוכלו לשים לב לדברים הבאים (התוצאות שלכם עשויות להשתנות בין הרצות):
- התוצאות ללא הפחתת שגיאות גרועות יותר. הרצת המעגל הגדול יותר כוללת ביצוע שערים נוספים, כך שיש יותר הזדמנויות לשגיאות להצטבר.
- הוספת dynamical decoupling עשויה להחמיר את הביצועים. זה לא מפתיע, מכיוון שהמעגל צפוף מאוד. Dynamical decoupling שימושי בעיקר כאשר יש פערים גדולים במעגל במהלכם קיוביטים יושבים במצב סרק ללא שערים המופעלים עליהם. כאשר פערים אלה אינם נוכחים, dynamical decoupling אינו יעיל, ויכול למעשה להחמיר את הביצועים עקב שגיאות בפולסים של dynamical decoupling עצמם. מעגל 10 הקיוביטים אולי היה קטן מדי עבורנו כדי לצפות בהשפעה זו.
- עם אקסטרפולציית רעש אפס, התוצאה טובה באותה מידה, או כמעט באותה מידה, כמו תוצאת 10 הקיוביטים, אם כי סרגל השגיאה גדול בהרבה. זה מדגים את העוצמה של טכניקת ZNE!
סיכום
במדריך זה, חקרתם אפשרויות הפחתת שגיאות שונות הזמינות עבור ה-Estimator primitive של Qiskit Runtime. פיתחתם תהליך עבודה באמצעות מעגל בעל 10 קיוביטים, ולאחר מכן הגדלתם אותו ל-50 קיוביטים. ייתכן שצפיתם שהפעלת אפשרויות דיכוי והפחתת שגיאות נוספות לא תמיד משפרת את הביצועים (באופן ספציפי, הפעלת dynamical decoupling במקרה זה). רוב האפשרויות מקבלות תצורה נוספת, אותה תוכלו לבדוק בעבודה שלכם!