דלג לתוכן הראשי

דיכוי שגיאות ועיצובן

דיכוי שגיאות יכול להתייחס לכל טכניקה שצופה מראש ומנסה להימנע מסוגים מסוימים של רעש ושגיאות. הכי קל להסביר זאת דרך דוגמאות קונקרטיות, אבל חשוב לזכור שהשיטות האלה אינן מוגבלות לדוגמאות המוצגות כאן, ושיטות חדשות ממשיכות להיחקר כל הזמן. לפעמים אי אפשר לדכא שגיאות, אבל אפשר להשפיע על האופן שבו הן מצטברות. אם נוכל לגרום לשגיאות להצטבר לאט יותר, אפשר לתאר זאת כדיכוי השגיאה הכוללת, אבל מדויק יותר לראות בכך עיצוב רעש. בשיעור הזה אנחנו מכסים דוגמאות קלאסיות לדיכוי שגיאות (dynamical decoupling) ולעיצוב/גיבוש רעש (Pauli twirling).

סרטון נלווה לשיעור הזה ישוגר בימים הקרובים, וישולב כאן.

ניתוק דינמי​

בואו נתחיל עם מצב פשוט מאוד על Qubit בודד, ונציג מודל רעש פשוט. זה לא מספיק כדי להסביר במלואו dynamical decoupling, אבל זה נותן לנו דוגמה ברורה. נניח שיש לנו Qubit שהוכן במצב סופרפוזיציה באמצעות שער Hadamard:

∣ψ⟩=H∣0⟩=12(∣0⟩+∣1⟩).|\psi\rangle = H|0\rangle = \frac{1}{\sqrt{2}}\left( |0\rangle +|1\rangle \right).

המצב הזה מוצג ויזואלית על כדור בלוך כפי שמוצג למטה משמאל. במקרה האידיאלי עם רעש אפס, ה-Qubit הזה היה נשאר במצב הזה עד לביצוע הפעולה הבאה. עם זאת, אנחנו יודעים מהשיעור הקודם שזה לא מה שאנחנו רואים בפועל. רעש גורם למידע הקוונטי להתדרדר.

A Bloch sphere with a state vector initially equal to the plus state. The state then precesses around the z axis while remaining in the xy plane, consistent with time evolution in the presence of a magnetic field oriented along the z axis.

רעש או צימוד סביבתי יכולים לגרום לשינוי בפאזה היחסית בין מצבי הבסיס. כלומר, ההסתברויות של ∣0⟩|0\rangle ו-∣1⟩|1\rangle לא משתנות, מכיוון שהערכים המוחלטים של המקדמים שלהן לא משתנים. במקום זאת, הפאזות של האמפליטודות משתנות, מה שמשנה את הרכיבים הממשיים והמדומים שלהן. כדי להפוך את הדיון הזה לקונקרטי יותר, בואו נבחן סוג אחד של אינטראקציה שיכולה לגרום לכך: צימוד לשדה מגנטי המכוון בכיוון Z: B⃗=(0,0,B0)\vec{B} = (0,0,B_0).

בואו נבחן מה קורה למצב ∣+⟩|+\rangle ככל שהזמן חולף:

∣ψ(t)⟩=e−iHt/ℏ∣ψ(0)⟩→e−iZωBt/2∣ψ(0)⟩|\psi(t)\rangle = e^{-iHt/\hbar}|\psi(0)\rangle \rightarrow e^{-iZ\omega_B t/2}|\psi(0)\rangle

כאן השתמשנו בעובדה ששדה מגנטי בכיוון Z גורם לפרצסיה סביב ציר ה-Z בתדירות ωB\omega_B שתלויה במומנט המגנטי האפקטיבי ובעוצמת השדה המגנטי. הפרטים פחות חשובים מהעובדה שהאינטראקציה הזו גורמת להתפתחות סביב ציר ה-Z, וכתוצאה מכך הצטברות פאזה הפוכה לשני מצבי הבסיס החישוביים. כאשר מפעילים את האופרטור הזה על כל איבר ב-∣ψ⟩|\psi\rangle, מתקבל הבא:

∣ψ(t)⟩=e−iZωBt/212(∣0⟩+∣1⟩)=12(e−iωBt/2∣0⟩+eiωBt/2∣1⟩)|\psi(t)\rangle = e^{-iZ\omega_B t/2}\frac{1}{\sqrt{2}}\left( |0\rangle +|1\rangle \right) =\frac{1}{\sqrt{2}}\left( e^{-i\omega_B t/2}|0\rangle +e^{i\omega_B t/2}|1\rangle \right)

הפאזה התלויה בזמן הזו מתאימה לפרצסיה סביב ציר ה-Z בתמונת כדור בלוך. זה מוצג בחצי הימני של האיור שלמעלה.

אילו ידענו שהאינטראקציה הזו מתרחשת בצורה מבוקרת, היינו יכולים לחזות אותה. כדי לדגם סוג רעש כזה, אנחנו במקום זאת בוחנים התפלגות של עוצמות שדה מגנטי אפשריות, שכל אחת מהן מתרחשת בהסתברות קלאסית מסוימת (מקורות רעש אחרים יצטרכו להיות מדוגמים אחרת). אנחנו צריכים לבחון מה קורה כאשר יש הסתברות קלאסית שאינה אפס שהמצב לא סובב כלל, וגם הסתברות מסוימת שהוא סובב בכמות קטנה, או אפילו בכמות גדולה. ההתפלגות הזו של סיבובים אפשריים היא הסיבה להתפזרות המצב בתמונת כדור בלוך למטה. אם יש יותר רעש דה-פאזה כזה, הפאזה תהיה פחות מוגדרת היטב. בגבול של דה-פאזה חזקה, ה-Qubit הופך לדה-פאזי לחלוטין, מה שמתאים לאובדן הקוהרנטיות הקוונטית שאוחסנה במצב.

Three panels, each showing a Bloch sphere. The first shows a pure quantum state vector, the plus state. The second shows a broadened, shorter region of state space, indicating a mixed state with imperfect phase information. The third shows a tiny region near the origin corresponding to complete dephasing.

כמובן, בניסוי אין לנו מושג איזה צימוד אקראי יתרחש. מה אפשר לעשות בקשר לזה?

נניח שהצימוד הסביבתי נשאר בערך קבוע לאורך מרווח זמן של 2t02t_0. בואו נבחן מה יקרה אם נפעל לפי המתכון הבא:

  • אפשרו לפאזה להשתנות למשך זמן t0t_0

  • הפעילו שער X על ה-Qubit

  • אפשרו לאותו צימוד סביבתי להתרחש למשך זמן נוסף של t0t_0

  • הפעילו שער X שני

אחרי ההתפתחות הראשונית בזמן, יהיה לנו בדיוק המצב שלמעלה. בהפעלת שער ה-X הראשון מתקבל:

X∣ψ(t0)⟩=12(e−iωBt0/2∣1⟩+eiωBt0/2∣0⟩)X|\psi(t_0)\rangle = \frac{1}{\sqrt{2}}\left( e^{-i\omega_B t_0/2}|1\rangle +e^{i\omega_B t_0/2}|0\rangle \right)

כעת כאשר המרווח השני של t0t_0 חולף, אותה אינטראקציה מתרחשת. אבל כעת האמפליטודות משויכות למצבי העצמי ההפוכים של Z, כלומר סימן הסיבוב סביב ציר ה-Z התהפך. לאחר שהמערכת מתפתחת בזמן למשך t0t_0 נוסף, מתקבל המצב:

e−iZωB(t−t0)/212(e−iωBt0/2∣1⟩+eiωBt0/2∣0⟩)=12(e−iωB(t−t0)/2eiωBt0/2∣1⟩+eiωB(t−t0)/2e−iωBt0/2∣0⟩)e^{-iZ\omega_B (t-t_0)/2}\frac{1}{\sqrt{2}}\left( e^{-i\omega_B t_0/2}|1\rangle +e^{i\omega_B t_0/2}|0\rangle \right) =\frac{1}{\sqrt{2}}\left( e^{-i\omega_B (t-t_0)/2}e^{i\omega_B t_0/2}|1\rangle +e^{i\omega_B (t-t_0)/2}e^{-i\omega_B t_0/2}|0\rangle \right)

ובהצבת t=2t0t = 2t_0 מתקבל:

∣ψ(2t0)⟩=12(∣1⟩+∣0⟩)|\psi(2t_0)\rangle=\frac{1}{\sqrt{2}}\left(|1\rangle +|0\rangle \right)

הפעלת שער ה-X האחרון לא משנה כלום במקרה הזה, אבל בדרך כלל היא הכרחית:

∣ψ(2t0)⟩=12(∣0⟩+∣1⟩)=∣ψ(t=0)⟩|\psi(2t_0)\rangle = \frac{1}{\sqrt{2}}\left(|0\rangle +|1\rangle \right) = |\psi(t=0)\rangle

שחזרנו את המצב הקוונטי המקורי, כולל הפאזה היחסית שלו. התהליך הזה הוא דוגמה פשוטה במיוחד ל-dynamical decoupling.

באופן כללי יותר, dynamical decoupling (DD) כולל הכנסה של כמה שערי Qubit-בודד כדי להפחית את ההשפעה של אינטראקציות עם מערכות מחוץ ל-Qubit (ניתוקו מהסביבה). המיקוד המחדש המלא של כל היסטוריות הפאזה האפשריות בחזרה למצב המקורי, כפי שמוצג למעלה, הוא במידה מסוימת אידיאלי, אבל עדיין תרחיש אפשרי. בואו נדון מתי DD שימושי ואילו הסתייגויות קיימות.

בדקו את ההבנה שלכם​

בטקסט שלמעלה, עברנו שלב אחר שלב על ההשפעה של רצף DD מסוג XX על המצב ההתחלתי ∣+⟩|+\rangle. בדקו האם אותם השלבים שלמעלה מחזירים גם את המצב ∣+i⟩|+i\rangle בחזרה למצבו ההתחלתי, תחת אותן הנחות של שדה מגנטי המשתנה לאט לאורך הכיוון z.

תשובה
∣ψ(t)⟩=e−iZωBt/212(∣0⟩+i∣1⟩)=12(e−iωBt/2∣0⟩+ieiωBt/2∣1⟩)|\psi(t)\rangle = e^{-iZ\omega_B t/2}\frac{1}{\sqrt{2}}\left( |0\rangle +i|1\rangle \right) =\frac{1}{\sqrt{2}}\left( e^{-i\omega_B t/2}|0\rangle +i e^{i\omega_B t/2}|1\rangle \right)

אחרי ההתפתחות הראשונית בזמן, יהיה לנו בדיוק המצב שלמעלה. בהפעלת שער ה-X הראשון מתקבל:

X∣ψ(t0)⟩=12(e−iωBt0/2∣1⟩+ieiωBt0/2∣0⟩)X|\psi(t_0)\rangle = \frac{1}{\sqrt{2}}\left( e^{-i\omega_B t_0/2}|1\rangle +i e^{i\omega_B t_0/2}|0\rangle \right)

כעת כאשר המרווח השני של t0t_0 חולף, אותה אינטראקציה מתרחשת. אבל כעת האמפליטודות משויכות למצבי העצמי ההפוכים של Z, כלומר סימן הסיבוב סביב ציר ה-Z התהפך. לאחר שהמערכת מתפתחת בזמן למשך t0t_0 נוסף, מתקבל המצב:

e−iZωB(t−t0)/212(e−iωBt0/2∣1⟩+ieiωBt0/2∣0⟩)=12(e−iωB(t−t0)/2eiωBt0/2∣1⟩+ieiωB(t−t0)/2e−iωBt0/2∣0⟩)e^{-iZ\omega_B (t-t_0)/2}\frac{1}{\sqrt{2}}\left( e^{-i\omega_B t_0/2}|1\rangle +i e^{i\omega_B t_0/2}|0\rangle \right) =\frac{1}{\sqrt{2}}\left( e^{-i\omega_B (t-t_0)/2}e^{i\omega_B t_0/2}|1\rangle +i e^{i\omega_B (t-t_0)/2}e^{-i\omega_B t_0/2}|0\rangle \right)

ובהצבת t=2t0t = 2t_0 מתקבל

∣ψ(2t0)⟩=12(∣1⟩+i∣0⟩)|\psi(2t_0)\rangle=\frac{1}{\sqrt{2}}\left(|1\rangle +i |0\rangle \right)

בהפעלת שער ה-X האחרון, מתקבל:

∣ψ(2t0)⟩=12(∣0⟩+i∣1⟩)=∣ψ(t=0)⟩=∣+i⟩|\psi(2t_0)\rangle = \frac{1}{\sqrt{2}}\left(|0\rangle +i |1\rangle \right) = |\psi(t=0)\rangle = |+i\rangle

מתי להשתמש ב-DD​

ההסתייגות הראשונה והברורה ביותר היא שהנחנו ערוץ דה-פאזה טהור בטיפול שלנו. אינטראקציות בעולם האמיתי בדרך כלל מייצרות תערובת של דה-פאזה ומנגנוני שגיאה אחרים. בטיפול שלמעלה, בחרנו להפעיל שערי X באופן ספציפי. זה מכונה רצף XX ב-dynamical decoupling. הרצף הספציפי הזה מתאים לשגיאות דה-פאזה טהורות. אבל יש רצפים אחרים שעשויים להיות ישימים יותר באופן כללי, כמו XY4 (המוצג בתרשים המעגל למטה) וה-XY8 המורכב יותר.

A quantum circuit showing a sequence of four quantum gates used in dynamical decoupling: an X gate, a Y gate, a second X gate, and a second Y gate.

הסתייגות נוספת היא ש-dynamical decoupling מוסיף שערי Qubit-בודד, שיכולים להוסיף שגיאות Qubit-בודד עקב אי-שלמות של השערים או אפילו crosstalk. שיעורי השגיאה של Qubit-בודד האלה בדרך כלל נמוכים בהרבה משגיאות שערים מרובי-Qubit, כך שבדרך כלל זו לא דאגה משמעותית, אבל זה משהו שכדאי לזכור אם הרבה Qubits משתמשים ב-DD פעמים רבות לאורך המעגל שלכם.

DD שימושי כאשר לצימוד החיצוני יש זמן להשפיע על מצב ה-Qubit. מכיוון ש-Qubits מבודדים היטב בקפידה וצימודים חיצוניים אמורים להיות חלשים, סוג הרעש הזה בולט ביותר כאשר Qubit נשאר בחוסר פעילות לתקופות ארוכות. עבור זמני חוסר פעילות קצרים, ההשפעה של DD עשויה להיות הוספת שגיאות שער Qubit-בודד תוך דיכוי מעט מאוד רעש; הנאמנות של המעגל שלכם עלולה למעשה לרדת.

המסקנה המרכזית: השתמשו ב-dynamical decoupling כאשר Qubits נשארים בחוסר פעילות לתקופות ארוכות מספיק, ושימו לב לסוג רצף ה-DD שבו משתמשים.

Dynamical decoupling באמצעות Qiskit​

בואו נבחן את השימוש ב-DD על ידי בדיקת המקרה של כמה Qubits שהוכנו במצב ∣+⟩|+\rangle, שנשארים לאחר מכן בחוסר פעילות לזמן ארוך. בהיעדר שגיאות, הכנת המצב מרובה ה-Qubits ∣ψ⟩=∣+⟩⊗N|\psi\rangle = |+\rangle^{\otimes N} ולאחר מכן הפעלת שער Hadamard לפני מדידה (ובכך מדידה בבסיס X) אמורה תמיד להניב 0. עם רעש, תוצאת המדידה תהיה 0 רק בחלק מהזמן ולא בהסתברות של 100%. אנחנו מחשבים את ערך התוחלת הממוצע של X על פני מספר Qubits. כלומר, אנחנו מתעניינים בבא:

f≡1N∑j=0N−1⟨Ψ∣Xj∣Ψ⟩f\equiv \frac{1}{N}\sum_{j=0}^{N-1}{\langle\Psi | X_j |\Psi\rangle}

כאשר Xj≡III..X...IIX_j \equiv III..X...II כאשר האופרטור X נמצא במיקום ה-jthj^{th} מימין ו-∣Ψ⟩|\Psi\rangle הוא המצב של המערכת כולה.

המעגל הזה מכיל עיכובים מכוונים, מה שקצת מאולץ לצורך מדד ביצועים (benchmark). עם זאת, מאוד נפוץ שמעגלים אמיתיים יכילו Qubits שנשארים בחוסר פעילות במהלך חלק מהביצוע. אפשר לחשוב על זה כעל מודל מפושט של מעגל מורכב יותר שבו כמה Qubits נשארים בחוסר פעילות לחלק מהחישוב. סוג הבדיקה הזה, המבוסס על ההתפתחות של מצבי ∣+⟩|+\rangle, מכונה לעיתים קרובות Ramsey benchmarking.

נתחיל בטעינת החבילות הנחוצות והגדרת התצורה של השירות.

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime
# Load key packages

from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
import numpy as np

# --------- Configuration ----------

service = QiskitRuntimeService() # assumes credentials are saved
backend = service.backend("ibm_fez") # adjust if needed

כעת נגדיר כמה פונקציות עזר. קודם כול, אנחנו רוצים להתייחס לנקודה לגבי זמני חוסר פעילות ארוכים. מה בדיוק פירוש המילה "ארוך" בהקשר הזה? היינו יכולים פשוט לדווח על זמן חוסר הפעילות במיקרו-שניות. עם זאת, מועיל לדעת כמה שערי שני-Qubits ניתן היה לבצע במהלך אותו מרווח זמן. זה הופך את זמני חוסר הפעילות לאינדיקטיביים יותר ישירות לעומק המעגל שיכול היה להתבצע באותה תקופה. פונקציית העזר הראשונה מקבלת את שער שני-ה-Qubits הטבעי ל-Backend וקובעת את משך הזמן של אותו שער.

הפונקציה השנייה פשוט יוצרת מעגל Ramsey (אחד שבו ה-Qubits נמצאים במצב ∣+⟩|+\rangle), מיישמת עיכוב, מסובבת באמצעות שער H, ולאחר מכן מודדת. זכרו ששער Hadamard (H) ממפה את ∣+⟩|+\rangle ל-∣0⟩|0\rangle, כך שמדידת ∣0⟩|0\rangle בבסיס Z אחרי ה-Hadamard מתאימה לכך שה-Qubit היה במצב ∣+⟩|+\rangle מיד לפני ה-Hadamard.

הפונקציה האחרונה שלנו ממירה את הספירות הגולמיות של תוצאות מדידה 0 ו-1 לערך תוחלת של X.

from typing import Tuple
from qiskit.providers import Backend
from numpy.typing import NDArray

# --------- Utilities ----------
def detect_twoq_gate_and_duration(
backend: Backend, pair: tuple[int, int] = (0, 1)
) -> Tuple[str, float]:
props = backend.properties()
candidates = ["cx", "ecr", "cz"]
for name in candidates:
try:
dur = props.gate_length(name, list(pair))
if dur is not None:
return name, dur
except Exception:
pass
raise RuntimeError(
"Could not find a two-qubit gate duration among cx/ecr/cz on this backend."
)

def make_multiqubit_ramsey_circuit(n: int, delay_dt_ticks: int) -> QuantumCircuit:
q = QuantumRegister(n, "q")
c = ClassicalRegister(n, "c")
qc = QuantumCircuit(q, c)

qc.h(q)
qc.barrier()

for i in range(n):
qc.delay(delay_dt_ticks, q[i], unit="dt")
qc.barrier()

qc.h(q)
qc.measure(q, c)
return qc

def counts_to_x_expectations(counts: dict[str, int], n: int) -> NDArray[np.float64]:
total = sum(counts.values())
if total == 0:
return np.zeros(n)

p0 = np.zeros(n, dtype=float)
for bitstring, cnt in counts.items():
bits_rev = bitstring[::-1]
for i in range(n):
if bits_rev[i] == "0":
p0[i] += cnt
p0 /= total
return 2.0 * p0 - 1.0

כעת אנחנו מציינים את פרטי הבדיקה שלנו, כולל מספר ה-Qubits ורצף השערים שישמש ב-DD (במקרה הזה XX). שימו לב במיוחד שאנחנו קובעים את רמת האופטימיזציה לאפס. בפועל, לעיתים קרובות תבחרו רמת אופטימיזציה גבוהה יותר; כאן אנחנו משתמשים ברמה 0 כדי להבטיח שההשפעות של השגיאות שאליהן DD מכוון יישארו גלויות. לבסוף, אנחנו קובעים את הזמן האופייני לשערי שני-Qubits ב-Backend הזה ומדפיסים כמה זמנים רלוונטיים.

n_qubits = 10 # number of qubits to test in parallel
shots = 4096
opt_level = 0 # we want to ignore optimization for now
num_steps = 6 # number of delay points
gates_per_step = 20 # "equivalent 2q gates" per step
dd_sequence = "XX" # "XX" for your request; you might try "XY4" too

# --------- Derive timing: dt and 2q gate time ----------
dt = backend.dt # seconds per dt
twoq_name, t2q = detect_twoq_gate_and_duration(backend, (0, 1)) # seconds
delay_unit_dt = int(round(t2q / dt)) # dt ticks equivalent to one 2q gate

print(f"Backend: {backend.name}")
print(f"dt = {dt*1e9:.3f} ns per tick")
print(f"Using 2q gate '{twoq_name}' with duration ~ {t2q*1e9:.1f} ns")
print(f"One 2q gate ≈ {delay_unit_dt} dt ticks")
Backend: ibm_fez
dt = 4.000 ns per tick
Using 2q gate 'cz' with duration ~ 68.0 ns
One 2q gate ≈ 17 dt ticks

כעת אנחנו בונים את המעגלים שלנו ומבצעים להם transpile עבור ה-Backend שבחרנו.

# --------- Build circuits for a sweep of delays ----------
pm = generate_preset_pass_manager(optimization_level=opt_level, backend=backend)

equiv_gates_list = [
j * gates_per_step for j in range(num_steps)
] # e.g., 0, 100, 200, ...
delay_dt_list = [int(round(delay_unit_dt * m)) for m in equiv_gates_list]
delay_us_list = [(dt * d) * 1e6 for d in delay_dt_list] # for printing/plotting

circuits = []
for delay_dt in delay_dt_list:
qc = make_multiqubit_ramsey_circuit(n_qubits, delay_dt)
qc_isa = pm.run(qc) # ISA-level scheduling/placement; DD is handled at runtime
circuits.append(qc_isa)

print("Delay sweep (approx microseconds):", [f"{t:.2f}" for t in delay_us_list])
Delay sweep (approx microseconds): ['0.00', '1.36', '2.72', '4.08', '5.44', '6.80']

כדאי להציג ויזואלית לפחות מעגל אחד כדי לוודא שקידדנו את המעגל שלנו עם המצבים והעיכובים הרצויים. יכול להיות קל יותר להציג ויזואלית את המעגל שלפני ה-transpile, qc, או שתוכלו לבדוק את זה שאחרי ה-transpile, qc_isa.

qc.draw("mpl")

Output of the previous code cell

ביצוע​

סוף סוף אנחנו מוכנים לבצע על חומרה. אנחנו משתמשים ב-Sampler כדי לקבל הרבה מדידות של כל Qubit, ונשתמש בו פעמיים: פעם אחת כאשר DD כבוי במפורש, ופעם אחת כאשר DD מופעל ובשימוש ברצף השערים XX.

# --------- Run: NO DD ----------
sampler = Sampler(mode=backend)
sampler.options.default_shots = shots
sampler.options.dynamical_decoupling.enable = False

job = sampler.run(circuits)
res_nodd = job.result()
job_id = job.job_id() # job id for dd off/false
print("job number for no dd is ", job_id)

# --------- Run: WITH DD (XX) ----------
sampler = Sampler(mode=backend)
sampler.options.default_shots = shots
sampler.options.dynamical_decoupling.enable = True
sampler.options.dynamical_decoupling.sequence_type = (
dd_sequence # "XX" first; you can try "XY4" too
)
sampler.options.dynamical_decoupling.scheduling_method = "alap"
sampler.options.dynamical_decoupling.extra_slack_distribution = "middle"

job = sampler.run(circuits)
res_dd = job.result()
job_id = job.job_id() # job id for dd on/XX
print("job number for dd using XX is ", job_id)

נוכל לחלץ את הספירות מהמעגלים השונים.

# --------- Extract counts per circuit ----------

from typing import Iterable, Any

def extract_counts_list(res: Iterable[Any]) -> list[dict[str, int]]:
counts_list: list[dict[str, int]] = []

for r in res: # each r corresponds to one circuit
counts: dict[str, int] = r.data.c.get_counts()
counts_list.append(counts)

return counts_list

counts_list_nodd = extract_counts_list(res_nodd)
counts_list_dd = extract_counts_list(res_dd)

עיבוד לאחר ביצוע​

כעת יש לנו את הספירות מהמדידה, אבל אנחנו רוצים להפוך את זה לערך התוחלת של X ולאחר מכן לחשב ממוצע של ערכי התוחלת האלה על פני כל ה-Qubits שבהם השתמשנו, כדי ללמוד על שימור מידע הפאזה. לשם כך אנחנו משתמשים בפונקציה counts_to_x_expectations שהוגדרה קודם.

# --------- Compute X-expectations and a scalar contrast ----------
# For each circuit (each delay), compute per-qubit <X> and average absolute contrast.

xexp_nodd = []
xexp_dd = []
contrast_nodd = []
contrast_dd = []

for counts in counts_list_nodd:
x_vec = counts_to_x_expectations(counts, n_qubits)
xexp_nodd.append(x_vec)
contrast_nodd.append(float(np.mean(np.abs(x_vec)))) # average |<X>| across qubits

for counts in counts_list_dd:
x_vec = counts_to_x_expectations(counts, n_qubits)
xexp_dd.append(x_vec)
contrast_dd.append(float(np.mean(np.abs(x_vec))))

# --------- Print a small summary ----------
print("\n=== Summary (average |<X>| per delay) ===")
for m, d_us, c0, c1 in zip(equiv_gates_list, delay_us_list, contrast_nodd, contrast_dd):
print(
f"Delay ~ {m:4d} * {twoq_name} (~{d_us:7.2f} µs): NoDD={c0: .3f}, DD({dd_sequence})={c1: .3f}"
)

# Optionally, inspect per-qubit values for the last delay point
print("\nPer-qubit <X> (abs) at the longest delay:")
print("NoDD:", np.round(np.abs(xexp_nodd[-1]), 3))
print("DD :", np.round(np.abs(xexp_dd[-1]), 3))
=== Summary (average |<X>| per delay) ===
Delay ~ 0 * cz (~ 0.00 µs): NoDD= 0.954, DD(XX)= 0.961
Delay ~ 20 * cz (~ 1.36 µs): NoDD= 0.827, DD(XX)= 0.927
Delay ~ 40 * cz (~ 2.72 µs): NoDD= 0.793, DD(XX)= 0.900
Delay ~ 60 * cz (~ 4.08 µs): NoDD= 0.729, DD(XX)= 0.876
Delay ~ 80 * cz (~ 5.44 µs): NoDD= 0.661, DD(XX)= 0.854
Delay ~ 100 * cz (~ 6.80 µs): NoDD= 0.586, DD(XX)= 0.826

Per-qubit <X> (abs) at the longest delay:
NoDD: [0.05 0.744 0.712 0.867 0.844 0.234 0.473 0.755 0.59 0.587]
DD : [0.583 0.89 0.921 0.88 0.908 0.832 0.893 0.773 0.807 0.773]

לבסוף, בואו נצייר גרף של התוצאות שלנו.

import matplotlib.pyplot as plt

fig, ax = plt.subplots()

# Add values with no DD
ax.scatter(
equiv_gates_list, contrast_nodd, c="blue", linestyle="-", label="No DD", alpha=0.7
)

## Add values with DD
ax.scatter(
equiv_gates_list, contrast_dd, c="red", linestyle="-", label="With DD", alpha=0.7
)

# Add labels and plot
ax.set_xlabel("Idle Time in # of 2-qubit gates")
ax.set_ylabel("<X>")
ax.legend()
ax.set_title("Dephasing and DD")
ax.grid(True)

plt.show()

Output of the previous code cell

כפי שאפשר לראות, ללא עיכוב מפורש ערכי התוחלת אכן קרובים ל-1, מה שהיינו מצפים אליו אילו כל מידע הפאזה היה משומר. אילו כל מידע הפאזה היה אבוד, לא הייתה שום העדפה לכך שהסיבוב הסופי יניב ∣0⟩|0\rangle ולא ∣1⟩|1\rangle, וערך התוחלת הממוצע היה מתקרב לאפס. בנתונים, אנחנו רואים שככל שזמני העיכוב גדלים, ערך התוחלת הממוצע של X יורד, ומתחיל להתקרב לאפס. שימו לב ש-DD היה יעיל מאוד כאן; ערכי התוחלת עם DD בדרך כלל טובים ביותר מ-20% (קרובים יותר ל-1) מהערכים ללא DD. אבל שימו לב גם שנקודת הנתונים המעוכבת הראשונה מתאימה לעיכוב השווה בערך ל-100 פעולות שער שני-Qubits. זה מחזק את הנקודה ש-DD הכי שימושי כאשר Qubits נשארים בחוסר פעילות לתקופות ארוכות יחסית.

בדקו את ההבנה שלכם​

אם נפעיל DD באמצעות XY4 על אותו מעגל כמו קודם, האם אתם מצפים שהתוצאות יהיו הרבה יותר טובות, הרבה יותר גרועות, או בערך אותו הדבר, בהשוואה ל-DD באמצעות XX? הסבירו.

תשובה

בערך אותו הדבר, אולי מעט יותר גרוע. המעגל שבו השתמשנו הכיל מצבים שסובבו למישור ה-XY. מצב כזה מאחסן מידע בעיקר בפאזה שלו, מה שהופך אותו לרגיש במיוחד לשגיאות דה-פאזה ולא להרפיית T1. XY4 עשוי לסייע במגוון רחב יותר של שגיאות, אבל XX כבר מותאם באופן אופטימלי לסייע למעגל שבו אנחנו משתמשים. XY4 עשוי להיות טוב באותה מידה, אבל לא יוסיף שום דבר משמעותי, או שהעובדה ש-XY4 מכיל יותר שערים עשויה לאפשר לשגיאות שער נוספות לגרום לתוצאות להיות מעט יותר גרועות.

התוצאה האחרונה השתמשה ברצף שערי ה-DD הפשוט ביותר, XX. בואו נראה איך ליישם רצף מורכב יותר, XY4. נגדיר Sampler בחלק הבא.

# --------- Run: WITH DD (XY4) ----------
dd_sequence = "XY4"

sampler = Sampler(mode=backend)
sampler.options.default_shots = shots
sampler.options.dynamical_decoupling.enable = True
sampler.options.dynamical_decoupling.sequence_type = dd_sequence
sampler.options.dynamical_decoupling.scheduling_method = "alap"
sampler.options.dynamical_decoupling.extra_slack_distribution = "middle"

job = sampler.run(circuits)
res_xy4 = job.result()
job_id = job.job_id() # job id for dd on/XY4
print("job number for dd using Xy4 is ", job_id)
job number for dd using Xy4 is d6k6ti860irc7395d3hg
# --------- Extract counts per circuit ----------

counts_list_xy4 = extract_counts_list(res_xy4)
# --------- Compute X-expectations and a scalar contrast ----------
# For each circuit (each delay), compute per-qubit <X> and average absolute contrast.
xexp_xy4 = []
contrast_xy4 = []

for counts in counts_list_xy4:
x_vec = counts_to_x_expectations(counts, n_qubits)
xexp_xy4.append(x_vec)
contrast_xy4.append(float(np.mean(np.abs(x_vec)))) # average |<X>| across qubits

# --------- Print a small summary ----------
print("\n=== Summary (average |<X>| per delay) ===")
for m, d_us, c0, c1, c2 in zip(
equiv_gates_list, delay_us_list, contrast_nodd, contrast_dd, contrast_xy4
):
print(
f"Delay ~ {m:4d} * {twoq_name} (~{d_us:7.2f} µs): NoDD={c0: .3f}, DD({dd_sequence})={c1: .3f}"
)

# Optionally, inspect per-qubit values for the last delay point
print("\nPer-qubit <X> (abs) at the longest delay:")
print("NoDD:", np.round(np.abs(xexp_nodd[-1]), 3))
print("DD XX :", np.round(np.abs(xexp_dd[-1]), 3))
print("DD XY4 :", np.round(np.abs(xexp_xy4[-1]), 3))
=== Summary (average |<X>| per delay) ===
Delay ~ 0 * cz (~ 0.00 µs): NoDD= 0.968, DD(XY4)= 0.968
Delay ~ 100 * cz (~ 6.80 µs): NoDD= 0.612, DD(XY4)= 0.839
Delay ~ 200 * cz (~ 13.60 µs): NoDD= 0.462, DD(XY4)= 0.705
Delay ~ 300 * cz (~ 20.40 µs): NoDD= 0.339, DD(XY4)= 0.580
Delay ~ 400 * cz (~ 27.20 µs): NoDD= 0.225, DD(XY4)= 0.481
Delay ~ 500 * cz (~ 34.00 µs): NoDD= 0.204, DD(XY4)= 0.393

Per-qubit <X> (abs) at the longest delay:
NoDD: [0.002 0.208 0.447 0.034 0.286 0.306 0.322 0.044 0.163 0.232]
DD XX : [0.433 0.669 0.526 0.516 0.572 0.284 0.303 0.055 0.365 0.208]
DD XY4 : [0.38 0.662 0.521 0.538 0.621 0.252 0.353 0.038 0.239 0.073]
import matplotlib.pyplot as plt

fig, ax = plt.subplots()

# Add values with no DD
ax.scatter(
equiv_gates_list, contrast_nodd, c="blue", linestyle="-", label="No DD", alpha=0.7
)

## Add values with DD using XX sequence
ax.scatter(
equiv_gates_list, contrast_dd, c="red", linestyle="-", label="With XX", alpha=0.7
)

## Add values with DD using XY4 sequence
ax.scatter(
equiv_gates_list,
contrast_xy4,
c="black",
linestyle="-",
label="With XY4",
alpha=0.7,
)

# Add labels and plot
ax.set_xlabel("Idle Time in # of 2-qubit gates")
ax.set_ylabel("<X>_av")
ax.legend()
ax.set_title("Dephasing and DD")
ax.grid(True)

plt.show()

Output of the previous code cell

כאן אנחנו רואים ש-XY4 לא שונה באופן ניכר מ-XX. הוא עשוי להיות מעט מאוד יותר גרוע בגלל שערים נוספים ברצף XY4, אבל חשוב יותר, כבר הסברנו למה XX יהיה בעל האפקט הרצוי בשימור הפאזה במיוחד עבור מצב כמו ∣+⟩|+\rangle. אין שום סיבה לחשוב שעבור מצב התחלתי כזה, רצף אחר ישפר את התוצאות.

בדקו את ההבנה שלכם​

ודאו שהרצף XY4 משאיר את המצב ללא שינוי עד כדי פאזה גלובלית.

תשובה
YXYX∣+⟩=YXYX(∣0⟩+∣1⟩)=YXY(∣0⟩+∣1⟩)=YX(i∣1⟩−i∣0⟩)=Y(i∣0⟩−i∣1⟩)=(−∣0⟩−∣1⟩)=−∣+⟩\begin{aligned} YXYX|+\rangle & = YXYX(|0\rangle+|1\rangle)\\ & = YXY(|0\rangle+|1\rangle)\\ & = YX(i|1\rangle-i|0\rangle)\\ & = Y(i|0\rangle-i|1\rangle)\\ & = (-|0\rangle-|1\rangle)\\ & =-|+\rangle \end{aligned}

Pauli twirling​

כדאי להתחיל בציון העובדה ש-Pauli twirling משמש לעיתים קרובות לא כטכניקת דיכוי שגיאות, אלא כטכניקת עיצוב שגיאות: היא גורמת לרעש/לשגיאות להתנהג אחרת, לפעמים בצורה צפויה יותר, כדי לאפשר שיטות אחרות. למרות ש-Pauli twirling לא מונע שגיאות, הוא עשוי למנוע את ההצטברות הקוהרנטית שלהן.

במעגל קוונטי, מקורות שונים ומרובים של שגיאה מצטרפים יחד. השגיאות יכולות להצטרף בדרכים שונות, בעיקר קוהרנטית ו-לא-קוהרנטית. הצטברות קוהרנטית של שגיאות פירושה שרעש או יישומים לא מושלמים נוטים כולם לדחוף שגיאות באותו הכיוון על פני שכבות ושערים מרובים. דוגמה לכך תהיה סיבוב-יתר קוהרנטי בעת הפעלת שער סיבוב.

בואו נבחן שער סיבוב אידיאלי, כמו Rx(θ0)R_x(\theta_0), שמסובב סביב ציר ה-X בדיוק ב-θ0\theta_0. כמובן, יישום השער אינו מושלם, והסיבוב בפועל עשוי להיות θ0+Δθ\theta_0+\Delta\theta עבור יישום אחד, ואף ייתכן ש-Δθ\Delta\theta תמיד באותו הסימן, ואולי אף דומה בגודלו על פני הפעלות רבות של Rx(θ)R_x(\theta). לכן, הפעלה חוזרת של שערי סיבוב יכולה להביא להצטברות קוהרנטית של סיבובי-היתר (או סיבובי-החסר) הרבים האלה, Δθ\Delta \theta.

הצטברות שגיאות לא-קוהרנטית היא בדיוק ההפך: שגיאות בכיוונים אקראיים עם סימנים אקראיים, כך ששגיאות בשכבות שונות לא תמיד מתאבכות באופן אדיטיבי, אלא לפעמים מתבטלות או מצטרפות בריבוע. ברור ששגיאות לא-קוהרנטיות מצטברות לאט יותר מבחינת ההשפעה הכוללת על מצב ה-Qubit. תרשים המחשה לכך מוצג באיור למטה. זוהי פישוט. שגיאות קוונטיות אמיתיות אינן מוגבלות למרחב קרטזי דו-ממדי; לא כל תרומות השגיאה יהיו באותו גודל, ויש עוד מורכבויות. אבל האינטואיציה של תמונה כזו שימושית: שגיאות קוהרנטיות נוטות להצטבר מהר יותר משגיאות לא-קוהרנטיות.

Two images. First, vectors denoting errors arranged in a line such that they add up coherently to a large error. Second, vectors in random directions being added to yield a smaller net effect, as in incoherent error accumulation.

לעיתים קרובות אפשר לקבל תוצאות בעלות נאמנות גבוהה יותר על ידי הפיכת הצטברות שגיאה קוהרנטית להצטברות שגיאה לא-קוהרנטית. דרך עיקרית להשיג זאת נקראת Pauli twirling.

Pauli twirling מתייחס להוספת צירופים של שערי Pauli P∈{X,Y,Z,I}P \in \{X, Y, Z, I\} לפני ואחרי פעולת שער רצויה UU באופן שבו P1UP2=UP_1 U P_2 = U. כאן, P1P_1 ו-P2P_2 אינם שערי Pauli בודדים, אלא אוספים של אופרטורי Pauli שפועלים לעיתים קרובות על מספר Qubits. לפעמים אפשר לראות טענה לפיה הפעולה של שערי ה-Pauli הנוספים "שקולה לזהות". אבל זה לא מדויק ועלול להטעות. שערי ה-Pauli מופרדים על ידי UU והמטרה היא להשאיר את הפעולה הלוגית של כל השערים שווה ל-UU. לפעמים UU נקרא ה-"payload" כדי להבדיל בין הפעולה המכוונת הזו לבין השערים שנוספו לצורך דיכוי. כמה דוגמאות ל-Pauli twirling סביב שער CNOT מוצגות למטה.

Four images showing pieces of four quantum circuits, each with two qubits. The first is a simple CNOT gate. The others each show a CNOT gate but surrounded by Pauli gates in a way that preserves the overall logic of a CNOT operation.

בואו נעבור על דוגמה אחת בלבד כדי לוודא ש-twirling משאיר את האפקט הלוגי של ה-payload ללא שינוי. מבלי לפגוע בכלליות, נניח שמצבי שני ה-Qubits הכוללים את q0q_0 ו-q1q_1 הם:

∣ψinit⟩=a∣00⟩+b∣01⟩+c∣10⟩+d∣11⟩|\psi_\text{init}\rangle = a|00\rangle + b|01\rangle + c|10\rangle + d|11\rangle

כמו תמיד, אנחנו משתמשים בקונבנציית סידור ה-Qubits ∣q1,q0⟩|q_1,q_0\rangle. הפעלת שער CX עם q1q_1 כמטרה מניבה

CX∣ψinit⟩=∣ψfinal⟩=a∣00⟩+b∣11⟩+c∣10⟩+d∣01⟩CX|\psi_\text{init}\rangle = |\psi_\text{final}\rangle = a|00\rangle + b|11\rangle + c|10\rangle + d|01\rangle

כעת בואו נבחן את המעגל השלישי המוצג, שמשתמש בשערי X עבור Pauli twirling. אפשר פשוט להכפיל את המטריצות יחד ולוודא שהן מניבות מטריצת CNOT. לחלופין, אפשר לעקוב אחרי הפעולה על מצב קוונטי שרירותי לאורך המעגל, כפי שאנחנו עושים למטה. המצבים בנקודות שונות במעגל סומנו a-d.

A CNOT operation on a two-qubit quantum circuit, surrounded by three X gates. Points are labeled a-d at the beginning of the circuit, after an X gate on the control qubit, after the CNOT gate, and after a final X gate is applied to each of the two qubits, respectively.

∣ψa⟩=a∣00⟩+b∣01⟩+c∣10⟩+d∣11⟩∣ψb⟩=a∣01⟩+b∣00⟩+c∣11⟩+d∣10⟩∣ψc⟩=a∣11⟩+b∣00⟩+c∣01⟩+d∣10⟩∣ψd⟩=a∣00⟩+b∣11⟩+c∣10⟩+d∣01⟩|\psi_a\rangle = a|00\rangle + b|01\rangle + c|10\rangle + d|11\rangle\\ |\psi_b\rangle = a|01\rangle + b|00\rangle + c|11\rangle + d|10\rangle\\ |\psi_c\rangle = a|11\rangle + b|00\rangle + c|01\rangle + d|10\rangle\\ |\psi_d\rangle = a|00\rangle + b|11\rangle + c|10\rangle + d|01\rangle

זה בדיוק ∣ψסופי⟩|\psi_\text{סופי}\rangle שקיבלנו קודם ללא twirling. ואכן, רצף השערים המסובב הזה משאיר את הפעולה הלוגית של ה-payload ללא שינוי. עם זאת, אם רצפי twirling תקפים שונים נבחרים באקראי משכבה לשכבה, הצטברות שגיאה קוהרנטית יכולה להפוך להצטברות שגיאה סטוכסטית (לא-קוהרנטית) באופן אפקטיבי. להבהרה, לא בוחרים תבנית twirling אחת ומשתמשים בה לאורך כל המעגל. במקום זאת, נבחרים רצפי twirling תקפים שונים עבור שכבות שונות. דוגמה עם שכבות סבוכות רבות עשויה להיראות כך.

A quantum circuit with four qubits and three CNOT gates in a ladder arrangement. In twirling, each of these CNOT gates is surrounded by a different set of Pauli gates.

בדקו את ההבנה שלכם​

ודאו כי גם ה-Pauli twirling בפאנל הרביעי באיור שלמעלה משאיר את האפקט הלוגי של ה-CNOT ללא שינוי.

תשובה

נעקוב אחרי הדוגמה שלמעלה ונראה שהפעולה על כל מצב שרירותי של שני Qubits שקולה לפעולת CNOT. נתייחס לנקודות המסומנות בתרשים הזה.

A quantum circuit with two qubits. The initial state is labeled &quot;a&quot;, then an X gate acts on qubit 0 and a Y gate acts on qubit 1. After that, the state is labeled &quot;b&quot;. Then a CNOT gate acts with qubit 0 as the control and qubit 1 as the target. After that, CNOT gate the state is labeled &quot;c&quot;. Finally, a Y gate acts on qubit 0 and a Z gate acts on qubit 1. The final state is labeled &quot;d&quot;.

∣ψa⟩=a∣00⟩+b∣01⟩+c∣10⟩+d∣11⟩∣ψb⟩=ai∣11⟩+bi∣10⟩−ci∣01⟩−di∣00⟩∣ψc⟩=ai∣01⟩+bi∣10⟩−ci∣11⟩−di∣00⟩∣ψd⟩=ai(+1)(−i)∣00⟩+bi(−1)(i)∣11⟩−ci(−1)(−i)∣10⟩−di(+1)(i)∣01⟩∣ψd⟩=a∣00⟩+b∣11⟩+c∣10⟩+d∣01⟩\begin{aligned} |\psi_a\rangle & = a|00\rangle + b|01\rangle + c|10\rangle + d|11\rangle\\ |\psi_b\rangle & = ai|11\rangle + bi|10\rangle - ci|01\rangle - di|00\rangle\\ |\psi_c\rangle & = ai|01\rangle + bi|10\rangle - ci|11\rangle - di|00\rangle\\ |\psi_d\rangle & = ai(+1)(-i)|00\rangle + bi(-1)(i)|11\rangle - ci(-1)(-i)|10\rangle - di(+1)(i)|01\rangle\\ |\psi_d\rangle & = a|00\rangle + b|11\rangle + c|10\rangle + d|01\rangle \end{aligned}

זה שקול לפעולה של CNOT כאשר Qubit 0 הוא הבקרה ו-Qubit 1 הוא המטרה.

האם תוכלו לחשוב על רצף Pauli twirling עבור שער ה-CNOT שלא מוצג למעלה?

תשובה

כן, יש רבים אחרים. דוגמה אחת היא ZtZ_t לפני ה-CNOT, ו-ZtZ_t ו-ZcZ_c אחרי ה-CNOT.

מתי להשתמש ב-Pauli twirling​

כפי שמוצג כאן, Pauli twirling מיושם רק על שערים מרובי-Qubits. יישום פרוטוקול דומה על שערי Qubit-בודד ידרוש לוגיקה שונה ובדרך כלל לא שימושי בפועל. Pauli twirling עצמו משתמש בכמה שערי Qubit-בודד (שערי Pauli). שערי ה-Pauli הנוספים עשויים להכניס יותר שגיאה ממה שהיה מתקבל מהפיכת כל הצטברות שגיאה קוהרנטית לאקראית. שיעורי השגיאה הקשורים לשערי שני-Qubits גדולים בהרבה מאלה הקשורים לשערי Qubit-בודד. יתר על כן, חלק משערי ה-Qubit-בודד הם non-Clifford, שלא ניתן לעשות להם twirling מלא. זו הסיבה ש-Qiskit כולל אפשרויות Pauli twirling שמבצעות twirling אוטומטית סביב שערי שני-Qubits, ולא סביב שערי Qubit-בודד.

זה היה מרומז באיור שלמעלה: Pauli twirling יושם סביב שערי ה-CX, אבל לא סביב שער ה-Hadamard.

בואו נראה שתי דוגמאות ל-Pauli twirling בפעולה.

Pauli twirling לדיכוי הצטברות קוהרנטית​

כדי לצפות כיצד Pauli twirling יכול להפוך הצטברות שגיאה קוהרנטית להצטברות לא-קוהרנטית איטית יותר, אנחנו רוצים מעגל ובר-תצפית שמתפקדים כמבחן עומס לשגיאה קוהרנטית. המטרה היחידה היא להפוך שגיאות שני-Qubits המצטברות באופן קוהרנטי לגלויות, ואז להראות כיצד Pauli twirling הופך את ההצטברות הקוהרנטית הזו לדעיכה סטוכסטית.

שערי CNOT (או CZ) הם מקור נפוץ לשגיאות קוהרנטיות. הניסוי הפשוט ביותר שאנחנו יכולים לבצע במקרה הזה הוא לאתחל מצב (למשל ∣+⟩|+\rangle), להפעיל שכבות של שערי CNOT זוגיים (תוך שימוש בעובדה ששני CNOTs מניבים זהות), ולבדוק כיצד שגיאות מצטברות ככל שמספר השכבות עולה, הן עם והן בלי Pauli twirling.

בר-התצפית המעניין הוא ⟨X⟩\langle X \rangle על Qubit בודד, שאותו אנחנו מציירים כפונקציה של מספר החזרות של זוגות CNOT.

# --- Imports ---

import numpy as np
from qiskit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2 as Sampler
# Build the circuit with CX/CX identity per layer

def identity_cx_pairs(layers: int) -> QuantumCircuit:
q = QuantumRegister(2, "q")
c = ClassicalRegister(1, "c") # we only measure q0
qc = QuantumCircuit(q, c, name=f"N={layers}")

# |+> on q0
qc.h(q[0])

for _ in range(layers):
qc.barrier()
qc.cx(q[0], q[1])
qc.cx(q[0], q[1])

# Measure in X basis: H then measure q0

qc.h(q[0])
qc.measure(q[0], c[0])

return qc

מכיוון שסיבבנו את הבסיס שלנו לפני המדידות, מדידה של ∣0⟩|0\rangle מתאימה לכך שהמצב היה ב-∣+⟩|+\rangle ממש לפני שער ה-Hadamard האחרון, ובדומה עבור ∣1⟩|1\rangle ו-∣−⟩|-\rangle. לכן, ערך התוחלת שלנו ⟨X⟩\langle X \rangle אפשר לחשב בפשטות מהספירות של ∣0⟩|0\rangle פחות הספירות של ∣1⟩|1\rangle.

# Compute <X> from SamplerV2 counts

def x_expect_from_counts(counts: dict[str, int]) -> float:
shots = sum(counts.values())
p0 = counts.get("0", 0) / shots
p1 = counts.get("1", 0) / shots
return p0 - p1 # <X> = P(0) - P(1) after H,measure

אנחנו בוחרים מספר סביר של שכבות CNOT שעליהן נאפשר לשגיאה להצטבר, בונים את המעגלים שלנו, ולאחר מכן מבצעים להם transpile.

# Choose the number of layers for the experiment
N_layers_list = [0, 1, 2, 3, 4, 5]

circuits = [identity_cx_pairs(n) for n in N_layers_list]

# Transpile to backend ISA so that primitives run native instructions
pm = generate_preset_pass_manager(backend=backend, optimization_level=0)
isa_circuits = [pm.run(c) for c in circuits]

חשוב לזכור שכל שכבה מורכבת מיותר משער שני-Qubits אחד. עקבו אחרי העומק של שני-Qubits לאחר ה-transpile באמצעות הפונקציה למטה.

# We can check the 2-qubit depths of any of our circuits like this:
print(
"two-qubit depth",
isa_circuits[5].decompose().depth(lambda instr: len(instr.qubits) > 1),
)
two-qubit depth 15
# Configure two Samplers: (A) no twirling, (B) gate twirling
# - No DD, no measurement twirling in both (to isolate gate twirling)
# ------------------------------
shots = 8192

# (A) No twirling
sampler_no_twirl = Sampler(mode=backend)
# Ensure no extra suppression/mitigation:
sampler_no_twirl.options.dynamical_decoupling.enable = False
# Be explicit about twirling:
sampler_no_twirl.options.twirling.enable_gates = False
sampler_no_twirl.options.twirling.enable_measure = (
False # TREX-style measurement twirling off
)
sampler_no_twirl.options.default_shots = shots # default shots for this primitive

# (B) Gate twirling ON
sampler_twirl = Sampler(mode=backend)
sampler_twirl.options.dynamical_decoupling.enable = False
sampler_twirl.options.twirling.enable_gates = True # <-- enable Pauli gate twirling
sampler_twirl.options.twirling.enable_measure = False
sampler_twirl.options.default_shots = shots

# (Optional) Inspect options dicts if you’re curious
# print(asdict(sampler_no_twirl.options))
# print(asdict(sampler_twirl.options))

כעת אנחנו מריצים את העבודות (jobs).

# Run both jobs; extract counts; compute <X>

# Helper to run a sampler and compute <X> per circuit
def run_and_x_expect(sampler: Sampler, circ_list: list[QuantumCircuit]) -> list[float]:
job = sampler.run(
circ_list
) # shots taken from options.default_shots unless overridden
result = job.result()
# For SamplerV2, use join_data().get_counts() to combine registers if needed
exp_vals = []
for pub in result:
counts = pub.join_data().get_counts()
exp_vals.append(x_expect_from_counts(counts))
return exp_vals

x_no_twirl = run_and_x_expect(sampler_no_twirl, isa_circuits)
x_twirl = run_and_x_expect(sampler_twirl, isa_circuits)

# ------------------------------
# 6) Print a small table
# ------------------------------
print("\nN_layers <X> (no twirl) <X> (gate twirl)")
for n, a, b in zip(N_layers_list, x_no_twirl, x_twirl):
print(f"{n:7d} {a:14.6f} {b:14.6f}")
N_layers <X> (no twirl) <X> (gate twirl)
0 0.984375 0.987549
1 0.934326 0.936523
2 0.844238 0.892822
3 0.712158 0.879395
4 0.592529 0.844971
5 0.449463 0.785156

לבסוף, אנחנו מציגים את התוצאות האלה ויזואלית.

import matplotlib.pyplot as plt

fig, ax = plt.subplots()

# Add values using XX
ax.scatter(
N_layers_list, x_no_twirl, c="blue", linestyle="-", label="No twirl", alpha=0.7
)

## Add values with XY4
ax.scatter(N_layers_list, x_twirl, c="red", linestyle="-", label="Twirled", alpha=0.7)

# Add labels and plot
ax.set_xlabel("CX layers")
ax.set_ylabel("<X>")
ax.legend()
ax.set_title("Pauli twirling")
ax.grid(True)

plt.show()

Output of the previous code cell

אנחנו יכולים לראות בבירור שהמעגל שעבר twirling מניב ערך תוחלת קרוב יותר לאידיאלי ⟨X⟩=1\langle X \rangle = 1. הדוגמה הזו מילאה את מטרתה, אבל בואו נעבור למשהו שימושי יותר: twirling בייצור מצבים סבוכים מאוד.

Pauli twirling בהכנת מצב GHZ​

הדוגמה הקודמת הראתה מקרה משכנע ל-Pauli twirling בהרס ההצטברות הקוהרנטית של שגיאות הקשורות לשערי CNOT. ייצור מצב GHZ משתמש בהרבה שערי CNOT כדי לייצר מצבים סבוכים מאוד שימושיים ליישומי מחשוב קוונטי רבים. בואו נבחן כיצד Pauli twirling מסייע בהקשר הזה, עם מצבי GHZ בגודל גדל.

# Imports if not already loaded in previous cells
# from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
# from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit

# Define a GHZ circuit building function, so that we can build GHZ states of increasing size.

def ghz_circuit(n: int) -> QuantumCircuit:
q = QuantumRegister(n, "q")
c = ClassicalRegister(n, "c")
qc = QuantumCircuit(q, c)

qc.h(q[0])
for i in range(n - 1):
qc.cx(q[i], q[i + 1])

qc.barrier()

qc.measure(q, c)
return qc

# Build a test state with 10 qubits to remind ourselves of GHZ structure.

num_qubits = 10

qc_ghz = ghz_circuit(num_qubits)
qc_isa = pm.run(qc_ghz)
qc_ghz.draw("mpl")

Output of the previous code cell

כעת אנחנו בונים את המעגלים שלנו ומבצעים להם transpile. במקרה הזה, אין לנו שערים חוזרים באופן מלאכותי שמצטמצמים לזהות. אז אנחנו יכולים לאפשר ל-pass manager לבצע קצת יותר אופטימיזציה בשבילנו. נגדיר אותו לרמה שלוש.

# Set up a pass manager

opt_level = 3
# --------- Build circuits for a sweep of delays ----------
pm = generate_preset_pass_manager(optimization_level=opt_level, backend=backend)

# Build GHZ circuits of increasing size.

nmin = 5
nmax = 15
circuits = []
for n in range(nmin, nmax):
qc = ghz_circuit(n)
qc_isa = pm.run(qc)
circuits.append(qc_isa)

אנחנו יכולים לראות שהאופטימייזר מיפה את המעגל המופשט שלנו ל-Qubits 123, 124, 136, 142 ו-143.

circuits[0].draw("mpl")

Output of the previous code cell

כדי להבין למה, בואו נסתכל על מפה של הפריסה של ה-Backend שלנו (בתמונה הזו, ibm_fez, אבל אפשר לבצע ניתוחים שווי-ערך על כל Backend).

A diagram of the layout of qubits on a quantum computer called ibm_fez. It shows how qubits are chosen to minimize swapping of information.

אנחנו רואים שה-Qubits נבחרו בשרשרת כדי למזער שערי swap ובכך את עומק המעגל. יתר על כן, כל שער CZ שמיושם במעגל הוא בין Qubits סמוכים. לבסוף, לכל חמשת ה-Qubits יש שיעורי שגיאה נמוכים יחסית, כולל שיעורי שגיאה של readout-assignment. אפשר לבדוק את שיעורי השגיאה האלה בכל Backend בדף משאבי מחשוב. מציאת פריסה כזו אינה קשה עבור שרשרת ליניארית פשוטה, אבל ככל שהבעיות הופכות מורכבות יותר, האופטימיזציה של פריסת המעגל הופכת קשה יותר ובעלת ערך רב יותר.

כעת אנחנו מגדירים את פרימיטיב ה-Sampler שלנו. אנחנו מכבים כלי דיכוי/הפחתה אחרים כדי להתמקד ב-Pauli twirling.

# Configure two Samplers: (A) no twirling, (B) gate twirling
# No DD, no measurement twirling in both (to isolate gate twirling)

shots = 8192

# (A) No twirling
sampler_no_twirl = Sampler(mode=backend)
# Ensure no extra suppression/mitigation:
sampler_no_twirl.options.dynamical_decoupling.enable = False
# Be explicit about twirling:
sampler_no_twirl.options.twirling.enable_gates = False
sampler_no_twirl.options.twirling.enable_measure = (
False # TREX-style measurement twirling off
)
sampler_no_twirl.options.default_shots = shots # default shots for this primitive

# (B) Gate twirling ON
sampler_twirl = Sampler(mode=backend)
sampler_twirl.options.dynamical_decoupling.enable = False
sampler_twirl.options.twirling.enable_gates = True # <-- enable Pauli gate twirling
sampler_twirl.options.twirling.enable_measure = False
sampler_twirl.options.default_shots = shots

# (Optional) Inspect options dicts if you’re curious
# print(asdict(sampler_no_twirl.options))
# print(asdict(sampler_twirl.options))

לבסוף, אנחנו מריצים את העבודות שלנו. באופן אופציונלי אפשר להדפיס את מספרי מזהה העבודה לצורך אחזור מאוחר יותר.

job = sampler_twirl.run(circuits)
res_ghz_twirl = job.result()
job_id = job.job_id() # job id for twirling on/true
print("job number for twirling the ghz prep is ", job_id)

job = sampler_no_twirl.run(circuits)
res_ghz_no_twirl = job.result()
job_id = job.job_id() # job id for twirling off/false
print("job number for NO twirling the ghz prep is ", job_id)
job number for twirling the ghz prep is d7h967bjne2c7393s0b0
job number for NO twirling the ghz prep is d7h96f7b91ec73aufing

אנחנו מחלצים את הספירות של כל מצב בסיס חישובי שנמדד עבור כל המעגלים, הן עם והן בלי twirling.

# --------- Extract counts per circuit ----------
def extract_counts_list(res):
counts_list = []
for r in res: # each r corresponds to one circuit
# r.data.<classical_register_name>.get_counts()
counts = r.data.c.get_counts()
counts_list.append(counts)
return counts_list

counts_list_ghz_twirl = extract_counts_list(res_ghz_twirl)
counts_list_ghz_no_twirl = extract_counts_list(res_ghz_no_twirl)
print(counts_list_ghz_twirl[0])
print(counts_list_ghz_twirl[4])
{'11111': 3440, '11101': 87, '00000': 3434, '10111': 87, '00001': 168, '10011': 4, '11000': 102, '00011': 39, '01111': 93, '00111': 111, '01000': 90, '11110': 235, '00110': 6, '00010': 75, '11011': 30, '11100': 42, '10000': 82, '00101': 7, '10001': 2, '11001': 6, '11010': 5, '01010': 3, '01110': 7, '00100': 14, '10101': 1, '10110': 8, '01001': 6, '10100': 1, '01101': 4, '01011': 1, '10010': 2}
{'111111111': 2683, '000000000': 2976, '111000000': 53, '110000000': 74, '111111100': 47, '111011111': 76, '111111110': 195, '111111000': 67, '000000010': 66, '011111110': 10, '011110001': 1, '101110111': 7, '101011111': 9, '111110111': 87, '111100000': 96, '111011000': 2, '010000000': 60, '111111010': 3, '100000000': 110, '111111011': 32, '000000111': 51, '000100000': 84, '000011111': 82, '000000001': 176, '100000010': 6, '111100001': 3, '000000011': 36, '101111111': 98, '100000011': 2, '001111111': 75, '000001000': 60, '000011101': 4, '110111111': 38, '111111101': 72, '111100100': 3, '000100010': 1, '001011101': 1, '000000110': 8, '110000011': 1, '000111111': 48, '000000100': 23, '000001111': 65, '010000101': 2, '000100001': 8, '111110000': 51, '010001111': 1, '111000111': 2, '111000001': 2, '000111100': 1, '011111111': 116, '111110001': 1, '000011110': 10, '000010000': 23, '000101111': 3, '000101110': 1, '011110111': 4, '010000111': 7, '111101111': 36, '001000000': 40, '010100000': 3, '101111110': 8, '110111000': 5, '000000101': 8, '010000010': 4, '000110001': 2, '110111110': 4, '111100010': 2, '111010111': 2, '000001110': 5, '111110101': 3, '001110000': 4, '101100000': 5, '001111110': 8, '100000001': 5, '111011110': 3, '111110100': 5, '110001011': 1, '001110111': 4, '000010110': 2, '111001111': 2, '000011000': 5, '010111110': 1, '000101000': 4, '101000000': 4, '100111111': 4, '111110011': 2, '000101100': 1, '101111100': 2, '111011100': 1, '001111011': 3, '011111011': 3, '110100000': 5, '000001011': 3, '111110110': 5, '111111001': 5, '000001001': 8, '010000001': 5, '011111101': 2, '001010111': 1, '011101110': 1, '110110110': 1, '001111100': 3, '100001000': 3, '001000011': 1, '001011111': 6, '110000010': 2, '110010000': 1, '010011111': 2, '111101101': 3, '101110000': 1, '111100111': 6, '010111111': 5, '110110000': 2, '011011111': 2, '110011111': 1, '110000111': 6, '001000001': 2, '001100000': 3, '101101111': 1, '000111110': 5, '111011101': 2, '100100000': 2, '101111101': 3, '001001111': 1, '001010000': 2, '111001000': 1, '011110000': 2, '011101111': 3, '000001100': 1, '110101000': 1, '011000000': 4, '111101000': 3, '110000001': 4, '000010111': 4, '011111100': 1, '111000010': 1, '101011000': 1, '101111000': 2, '001101111': 1, '010001100': 1, '000100011': 1, '111110010': 1, '111101110': 1, '100001111': 1, '100011011': 1, '010110111': 1, '001110110': 2, '100000100': 1, '001000110': 1, '100011111': 2, '010010001': 1, '111010110': 1, '011110110': 1, '000111000': 2, '000100111': 1, '011010000': 1, '001111000': 2, '100010000': 1, '011111000': 2, '110111100': 2, '110110111': 1, '110001000': 1, '000110111': 1, '000101011': 1, '000110000': 1, '011100000': 3, '001000010': 1, '001001000': 1, '001000111': 1, '001111101': 3, '111101011': 1, '111010000': 1, '100000101': 1, '000010010': 1, '001011110': 1, '000011011': 1, '111101100': 1}

אנחנו יודעים שההתפלגות האידיאלית של מצב GHZ היא זו שבה מחצית מהירי (shots) מחזירים ∣0⟩⊗N|0\rangle^{\otimes N} והמחצית השנייה מחזירה ∣1⟩⊗N|1\rangle^{\otimes N}. נבנה את זה לצורך השוואה.

ideal_dist = []
for n in range(nmin, nmax):
ideal_dist.append({"0" * n: int(shots / 2), "1" * n: int(shots / 2)})

כעת אנחנו משתמשים בנאמנות הלינגר (Hellinger fidelity) כמדד לאיכות המצב הסופי שלנו.

from qiskit.quantum_info import hellinger_fidelity

num_qubits = []
fidelities_twirl = []
fidelities_no_twirl = []
for n in range(len(ideal_dist)):
num_qubits.append(nmin + n)
fidelities_twirl.append(hellinger_fidelity(counts_list_ghz_twirl[n], ideal_dist[n]))
fidelities_no_twirl.append(
hellinger_fidelity(counts_list_ghz_no_twirl[n], ideal_dist[n])
)

לבסוף אנחנו מציירים גרף של התוצאות שלנו.

import matplotlib.pyplot as plt

fig, ax = plt.subplots()

# Add values using no twirling
ax.scatter(
num_qubits,
fidelities_no_twirl,
c="blue",
linestyle="-",
label="No twirl",
alpha=0.7,
)

## Add values with twirling
ax.scatter(
num_qubits, fidelities_twirl, c="red", linestyle="-", label="Twirled", alpha=0.7
)

# Add labels and plot
ax.set_xlabel("Qubits in GHZ state")
ax.set_ylabel("Hellinger fidelity")
ax.legend()
ax.set_title("Pauli twirling in GHZ states")
ax.grid(True)

plt.show()

Output of the previous code cell

התוצאות באמצעות Pauli twirling אינן טובות יותר (ואפילו מעט גרועות יותר) מאשר ללא twirling. מה קרה?

שני דברים קרו. ראשית, Pauli twirling לא מפחית את כמות הרעש הכוללת — אלא, הוא מעצב מחדש שגיאות קוהרנטיות ושיטתיות לשגיאות מסוג Pauli סטוכסטיות, כך שגדילת השגיאה הופכת לצפויה וניתנת למידול. מעולם לא הייתה הבטחה להפחתת שגיאות, למעט במקרים מיוחדים.

שנית, במעגלי GHZ, חלק מהשגיאות הקוהרנטיות יכולות להתבטל חלקית או לפעול כמו הזחות פאזה שפירות בגלל הסימטריה של מבנה ה-GHZ; twirling מסיר את ההגנה המקרית הזו ומחליף אותה ברעש Pauli סטוכסטי לא-מתואם, כך שנאמנות ה-GHZ הופכת מעט יותר גרועה תחת twirling.

הטענה השנייה הזו דורשת קצת הסבר. הטענה אינה שמעגלי GHZ מוגנים מפני כל סוגי ההצטברות של שגיאה קוהרנטית, אלא רק מפני סוגים מסוימים — ושבמקרים האלה ה-twirling הורס את ההגנה. באופן ספציפי, בואו נבחן סיבוב-יתר קוהרנטי הקשור לשערי CX. נקרא לשער ה-CX האמיתי עם סיבוב-היתר CX~\tilde{CX}:

CX~≡e−iϵK⋅CX\tilde{CX}\equiv e^{-i\epsilon K} \cdot CX

כאשר KK הוא מכפלה כלשהי של אופרטורי פאולי, כגון XX, ZZ, Z⊗XZ \otimes X, Z⊗NZ^{\otimes N}, וכן הלאה. עבור מצבים כלליים, כל אחד מסיבובי היתר הללו עשוי להשפיע על הסטטיסטיקה של המדידה (ומכאן גם על מדדי הנאמנות של המצב). עם זאת, תת-קבוצה מהם משאירה רבים מהאובזרבלים הסטנדרטיים של GHZ ללא שינוי, כולל אופרטורים כמו ZiZjZ_i Z_j ו-Z⊗NZ^{\otimes N}. בהקשר של הכנת מצב GHZ, שגיאות סיבוב היתר הרלוונטיות מסוג זה יהיו:

CX~≡e−iϵZcZt⋅CX\tilde{CX}\equiv e^{-i\epsilon Z_c Z_t} \cdot CX

הכנת מצב GHZ המלא של NN qubits תיראה כך:

∣ψGHZ⟩=∏j=0N−2e−iϵZjZj+1⋅CXj,j+1H⊗N∣0⊗N⟩|\psi_\text{GHZ}\rangle=\prod_{j=0}^{N-2}{e^{-i\epsilon Z_j Z_{j+1}} \cdot CX_{j,j+1}}H^{\otimes N}|0^{\otimes N}\rangle

לאחר הכנת מצב ה-GHZ, המצב הוא באופן אידיאלי:

∣GHZ⊗N⟩=12(∣0…0⟩+∣1…1⟩).|\text{GHZ}^{\otimes N}\rangle=\frac{1}{\sqrt{2}}\left(|0\dots 0\rangle + |1\dots 1\rangle\right).

מצב זה הוא מצב עצמי משותף של קבוצה גדולה של אופרטורי פאולי, כולל הבאים:

ZiZjfor all i≠j,Z_i Z_j \quad \text{for all } i \neq j,

עם ערך עצמי +1+1. כתוצאה מכך, אופרטור מהצורה e−iϵZiZje^{-i\epsilon Z_i Z_j} פועל על מצב ה-GHZ כהכפלה בגורם הפאזה e−iϵe^{-i\epsilon}, שאינו משפיע על האובזרבלים הסטנדרטיים של GHZ כגון זוגיות, X⊗NX^{\otimes N} קולקטיבי, או אכלוסי הבסיס החישובי. לכן, למרות ששגיאות סיבוב היתר e−iϵZjZj+1e^{-i\epsilon Z_j Z_{j+1}} הן קוהרנטיות ושיטתיות, הן בלתי נראות למעשה עבור המדידות המשמשות להערכת הנאמנות של GHZ. במובן זה, מעגל ה-GHZ נהנה מהגנת קוהרנטיות מקרית: שגיאות CX קוהרנטיות מסוימות מתחלפות עם המבנה של המצב המוכן ולכן אינן פוגעות בביצועים הנמדדים.

סיבוב פאולי משנה את המצב הזה באופן יסודי. הסיבוב אינו משמר את שגיאת סיבוב היתר הקוהרנטית כתהליך ZjZj+1Z_jZ_{j+1} דטרמיניסטי. במקום זאת, הוא ממיר את ערוץ השגיאה הקוהרנטי לערוץ פאולי סטוכסטי אפקטיבי. כתוצאה מכך, ערוץ השגיאה כולל כעת איברים כגון XX, YY, X⊗ZX \otimes Z, ו-Y⊗XY \otimes X, שאינם מתחלפים עם המייצבים של GHZ.

כאשר שגיאות פאולי סטוכסטיות אלה מתרחשות, הן יוצרות תקלות אמיתיות של היפוך ביט והיפוך פאזה ב-qubits בודדים או בזוגות qubits. שגיאות אלה מוציאות את המצב מתת-המרחב המייצב של GHZ, מפחיתות את ההתאבכות בין ∣0…0⟩|0\dots 0\rangle ל-∣1…1⟩|1\dots 1\rangle, ומורידות ישירות את הנאמנות של GHZ ואת אותות הזוגיות הרב-qubit. במילים אחרות, סיבוב פאולי מסיר את המבנה הקוהרנטי של השגיאה אך גם מסיר את הביטול המבוסס על סימטריה שבעבר הפך את השגיאות הללו לבלתי מזיקות. התוצאה היא מצב GHZ מעט גרוע יותר — לא משום שהסיבוב מוסיף רעש, אלא משום שהוא הופך שגיאה קוהרנטית שברובה בלתי מזיקה לשגיאות סטוכסטיות מזיקות באמת.

הדוגמה הזו ממחישה לקח חשוב: סיבוב פאולי אינו אסטרטגיית שיפור אוניברסלית. הוא מועיל ביותר כאשר שגיאות קוהרנטיות מצטברות לאורך מעגל באופן ההורס את הנאמנות הנדרשת. במעגלים סימטריים מאוד כמו הכנת מצב GHZ, חלק מהשגיאות הקוהרנטיות מיושרות באופן טבעי עם המייצבים של המצב, ואקראה מכוונת שלהן עלולה לבטל את ההגנה המקרית הזו.

שילוב שיטות​

ניתן באופן עקרוני לשלב סיבוב פאולי עם ניתוק דינמי (dynamical decoupling). לא השתמשנו ב-DD במקרה הזה משתי סיבות: ראשית, לא אמורות להיות תקופות המתנה ארוכות במיוחד במעגל הזה. שנית, מצבי GHZ אינם שומרים את רוב המידע שלהם בקוהרנטיות הפאזה של qubit בודד, בפרט אם אנו בוחנים את הנאמנות רק במונחים של ספירות מחרוזות ביט (bitstring), ולא במונחי הפאזה בין מחרוזות הביט הרצויות ∣0⟩⊗N|0\rangle^{\otimes N} ו-∣1⟩⊗N|1\rangle^{\otimes N}.

אם היינו מרחיבים את הכנת ה-GHZ ל-100+ qubits, אזי העיכובים במדידת ה-qubits הראשונים/המוקדמים עשויים להיות ארוכים דיים כדי ש-DD יהיה שימושי. זה נכון במיוחד כאשר לוקחים בחשבון את העומק המתורגם (transpiled) של המעגל.

סיבוב פאולי במבנה הד RTZ​

הדוגמה האחרונה שלנו לסיבוב פאולי בהקשר של הפחתת הצטברות שגיאות קוהרנטיות משתמשת במעגל עם שכבות של צירופי X-CZ-X-CZ. זהו רצף שערים ידוע היטב שמשמש לעיתים קרובות לביטול איברי שגיאת ZZ קוהרנטיים בשערי שני qubits, גם ללא סיבוב. אך עם סיבוב, נוכל להפחית או לאקרר צורות נוספות של הצטברות שגיאה קוהרנטית.

נתחיל בהגדרת פונקציה לבניית מעגלים דמויי-RTZ עם מספר משתנה של שכבות.

from qiskit import QuantumCircuit

def rtz_echo_circuit(n_qubits: int, depth: int) -> QuantumCircuit:
"""
Construct an RTZ echo-style circuit.

Args:
n_qubits: Number of qubits in the circuit.
depth: Number of repeated echo layers.

Returns:
A QuantumCircuit implementing the echo sequence with measurements.
"""
q = QuantumRegister(n_qubits, "q")
c = ClassicalRegister(n_qubits, "c")
qc = QuantumCircuit(q, c)

for _ in range(depth):
qc.h(q)

for i in range(0, n_qubits - 1, 2):
qc.cz(q[i], q[i + 1])

qc.x(q)

for i in range(1, n_qubits - 1, 2):
qc.cz(q[i], q[i + 1])

qc.h(q)

qc.measure(q, c)
return qc

כעת נבנה מעגלים עם מספר גדל והולך של שכבות, עד לעומק כולל סביר של שני qubits לאחר תרגום (transpiled).

from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

circuits = []
qcs = []
depths = list(range(3, 27, 4))
n_qubits = 10

opt_level = 0
pm = generate_preset_pass_manager(
optimization_level=opt_level,
backend=backend,
initial_layout=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
)

for d in depths:
qc = rtz_echo_circuit(n_qubits, d)
qcs.append(qc)
qc_isa = pm.run(qc)
circuits.append(qc_isa)
# We can check the 2-qubit depths of any of our circuits like this:
two_qubit_depths = []
for n in range(len(circuits)):
two_qubit_depths.append(
circuits[n].decompose().depth(lambda instr: len(instr.qubits) > 1)
)
print(two_qubit_depths)
[6, 14, 22, 30, 38, 46]
qcs[0].draw("mpl")
# circuits[0].draw("mpl")

Output of the previous code cell

בקצה העליון, חלק מאלה עמוקים למדי. נשתמש ב-AerSimulator ללא מודל רעש כדי לקבל את המצבים האידיאליים בסוף כל אחד מהמעגלים הללו. לאחר מכן נוכל להשוות את התוצאות שלנו ממחשבים קוונטיים אמיתיים באמצעות הנאמנות של הלינגר (Hellinger fidelity).

from qiskit_aer import AerSimulator

sim = AerSimulator()
ideal_results = sim.run(circuits, shots=8192).result()
ideal_counts = ideal_results.get_counts()

כעת נגדיר SamplerV2 עם סיבוב, ואחד ללא סיבוב.

from qiskit_ibm_runtime import SamplerV2 as Sampler

shots = 8192

# --- No Twirling ---
sampler_no = Sampler(mode=backend)
sampler_no.options.twirling.enable_gates = False
sampler_no.options.twirling.enable_measure = False
sampler_no.options.default_shots = shots

# --- With Twirling ---
sampler_tw = Sampler(mode=backend)
sampler_tw.options.twirling.enable_gates = True
sampler_tw.options.twirling.enable_measure = False
# sampler_tw.options.twirling.num_randomizations = "auto"
sampler_tw.options.twirling.num_randomizations = 32
sampler_tw.options.twirling.strategy = "active-circuit"
sampler_tw.options.default_shots = shots

כעת נריץ את העבודות שלנו.

# Each job took 17 sec (34 sec total) on ibm_fez. Your times might vary.

job_no = sampler_no.run(circuits)
job_tw = sampler_tw.run(circuits)

res_no = job_no.result()
res_tw = job_tw.result()

נקבל את הספירות מכל אחת מההרצות על מחשב קוונטי אמיתי.

counts_no = [r.data.c.get_counts() for r in res_no]
counts_tw = [r.data.c.get_counts() for r in res_tw]

כעת נמצא את הנאמנות של הלינגר על ידי השוואת כל אחת מההרצות הללו לתוצאות ה-AerSimulator נטולות הרעש.

from qiskit.quantum_info import hellinger_fidelity

f_no = [hellinger_fidelity(counts_no[i], ideal_counts[i]) for i in range(len(circuits))]

f_tw = [hellinger_fidelity(counts_tw[i], ideal_counts[i]) for i in range(len(circuits))]

כעת נציג את התוצאות שלנו בצורה חזותית.

import matplotlib.pyplot as plt

plt.figure(figsize=(8, 5))
plt.plot(two_qubit_depths, f_no, "o-", label="No Twirling")
plt.plot(two_qubit_depths, f_tw, "o-", label="With Twirling")
plt.xlabel("Two-qubit transpiled depth")
plt.ylabel("Hellinger Fidelity")
plt.title("RTZ Echo Circuit: Twirling vs No Twirling")
plt.legend()
plt.grid(True)
plt.show()

Output of the previous code cell

לאורך השיעור הזה, בחנו מקרים שבהם סיבוב פאולי משמש להגבלת הצטברות שגיאות קוהרנטיות, הקשר שבו ניתן לתאר אותו כדיכוי שגיאות. עם זאת, סיבוב פאולי שימושי לעיתים קרובות יותר ככלי לעיצוב מחדש של התנהגות השגיאה, המרת שגיאות קוהרנטיות לצורה שניתן לחזות ולמדל בקלות רבה יותר. התועלת בכך תהפוך לברורה יותר בהקשר של טכניקות הפחתת שגיאות כגון אקסטרפולציה לרעש אפס (zero-noise extrapolation, ZNE), המסתמכות על שינוי רעש שניתן לחזות באופן סביר. הנושא נדון בשיעור הבא.