שזירה ארוכת טווח עם מעגלים דינמיים
אומדן שימוש: 4 דקות על מעבד Heron r2. (הערה: זהו אומדן בלבד. זמן הריצה שלך עשוי להשתנות.)
תוצאות למידה
לאחר השלמת מדריך זה, תלמד את הדברים הבאים:
- כיצד לממש שער CNOT ארוך טווח באמצעות מעגלים דינמיים עם מדידות בתוך המעגל (MCMs) והזנה קדימה קלאסית;
- כיצד לממש את השער השווה ערך באמצעות גישה יוניטרית מבוססת SWAP;
- כיצד להשוות את שתי הגישות על ידי מדידת נאמנות השער כפונקציה של מרחק ה-qubit.
דרישות מוקדמות
מומלץ שהמשתמשים יהיו מכירים את הנושאים הבאים לפני עבור מדריך זה:
- מושגי חישוב קוונטי בסיסיים, כולל מצבי Bell, שזירה ושערים קוונטיים;
- היכרות עם מעגלים דינמיים (מדידות בתוך המעגל והזנה קדימה קלאסית);
- ידע בסיסי ב-Qiskit SDK וב-Qiskit Runtime, וגישה ל-חשבון IBM Quantum®.
רקע
שזירה ארוכת טווח בין qubits מרוחקים היא אתגר במכשירים עם קישוריות מוגבלת. מדריך זה מראה כיצד מעגלים דינמיים יכולים ליצור שזירה כזו על ידי מימוש שער controlled-X ארוך טווח (LRCX) באמצעות פרוטוקול מבוסס מדידה.
בעקבות הגישה של Elisa Bäumer ועמיתיה ב-1, השיטה משתמשת במדידה בתוך המעגל והזנה קדימה כדי להשיג שערים בעומק קבוע ללא קשר למרחק בין ה-qubits. היא יוצרת זוגות Bell ביניים, מודדת qubit אחד מכל זוג, ומפעילה שערים מותנים קלאסית כדי להפיץ את השזירה על פני ההתקן. זה נמנע משרשראות SWAP ארוכות, ומפחית גם את עומק המעגל וגם את החשיפה לשגיאות שערים דו-qubit.
במחברת זו, אנחנו מתאימים את הפרוטוקול לחומרה של IBM Quantum ומבצעים benchmark של ביצועיו כפונקציה של הפרדת הבקרה-מטרה, ומשווים אותו למימוש בסיסי יוניטרי מבוסס SWAP.
דרישות
לפני התחלת מדריך זה, ודא שיש לך את המותקנים הבאים:
- Qiskit SDK v2.0 או מאוחר יותר, עם תמיכה ב-visualization
- Qiskit Runtime v0.37 או מאוחר יותר (
pip install qiskit-ibm-runtime) - Qiskit Aer v0.17 או מאוחר יותר (
pip install qiskit-aer)
הגדרה
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit.classical import expr
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.visualization import plot_circuit_layout
from qiskit_ibm_runtime import (
QiskitRuntimeService,
Batch,
SamplerV2 as Sampler,
)
import matplotlib.pyplot as plt
import numpy as np
דוגמת סימולטור בקנה מידה קטן
לפני הרצה על ה-QPU האמיתי, נוודא ששני המעגלים הדינמיים והיוניטריים מייצרים מצב Bell אידיאלי על סימולטור ללא רעש. נשתמש ב-Qiskit Runtime Sampler עם AerSimulator כמצב ה-backend, במרחק קטן של 6.
שלב 1: מיפוי קלטים קלאסיים לבעיה קוונטית
כעת נממש שער CNOT ארוך טווח בין שני qubits מרוחקים, בעקבות בניית המעגל הדינמי המוצגת להלן (מותאם מאיור 1a בהפניה 1). הרעיון המרכזי הוא להשתמש ב-"אפיק" של qubits עזר, מאותחלים ל-, כדי לתווך טלפורטציית שער ארוכת טווח.

כפי שמודגם באיור, התהליך פועל באופן הבא:
- הכנת שרשרת של זוגות Bell המחברת את ה-qubits של הבקרה והמטרה דרך עזרים ביניים.
- ביצוע מדידות Bell בין qubits שכנים לא-משוזרים, החלפת שזירה צעד אחר צעד עד שהבקרה והמטרה חולקות זוג Bell.
- שימוש בזוג Bell זה לטלפורטציית שער, הפיכת CNOT מקומי ל-CNOT ארוך טווח דטרמיניסטי בעומק קבוע.
גישה זו מחליפה שרשראות SWAP ארוכות בפרוטוקול בעומק קבוע, מפחיתה חשיפה לשגיאות שערים דו-qubit והופכת את הפעולה לסקלבילית עם גודל ההתקן.
בהמשך, נעבור תחילה על המימוש הדינמי של מעגל LRCX. בסוף, נספק גם מימוש מבוסס יוניטרי להשוואה, כדי להדגיש את היתרונות של מעגלים דינמיים בהקשר זה.
אתחול מעגל
נתחיל עם בעיה קוונטית פשוטה שתשמש כבסיס להשוואה. באופן ספציפי, נאתחל מעגל עם qubit בקרה באינדקס 0 ונחיל עליו שער Hadamard. זה מייצר מצב סופרפוזיציה שכאשר הוא מלווה בפעולת controlled-X, מייצר מצב Bell בין ה-qubits של הבקרה והמטרה.
בשלב זה, אנחנו עדיין לא בונים את ה-LRCX עצמו. במקום זאת, המטרה שלנו היא להגדיר מעגל התחלתי ברור ומינימלי המדגיש את תפקיד ה-LRCX. בשלב 2, נראה כיצד ניתן למימש את ה-LRCX כאופטימיזציה באמצעות מעגלים דינמיים, ונשווה את הביצועים שלו ליוניטרי שווה ערך. חשוב לציין שפרוטוקול ה-LRCX ניתן ליישום על כל מעגל התחלתי. כאן אנחנו משתמשים בהגדרת Hadamard פשוטה זו לצורך הבהרה.
distance = 6 # The distance of the CNOT gate, with the convention that a distance of zero is a nearest-neighbor CNOT.
def initialize_circuit(distance):
assert distance >= 0
control = 0 # control qubit
n = distance # number of qubits between target and control
qr = QuantumRegister(
n + 2, name="q"
) # Circuit with n qubits between control and target
cr = ClassicalRegister(
2, name="cr"
) # Classical register for measuring control and target qubits
k = int(n / 2) # Number of Bell States to be used
allcr = [cr]
if (
distance > 1
): # This classical register will be used to store ZZ measurements.
# It is only used for long-range CX gates with distance > 1
c1 = ClassicalRegister(
k, name="c1"
) # Classical register needed for post processing
allcr.append(c1)
if (
distance > 0
): # This classical register will be used to store XX measurements.
# It is only used if distance > 0
c2 = ClassicalRegister(
n - k, name="c2"
) # Classical register needed for post processing
allcr.append(c2)
qc = QuantumCircuit(qr, *allcr, name="CNOT")
# Apply a Hadamard gate to the control qubit such that the
# long-range CNOT gate will prepare a
# Bell state (|00> + |11>)/sqrt(2)
qc.h(control)
return qc
qc = initialize_circuit(distance)
qc.draw(fold=-1, output="mpl", scale=0.5)
שלב 2: אופטימיזציה של הבעיה להפעלה על חומרה קוונטית
בשלב זה, נראה כיצד לבנות את מעגל LRCX באמצעות מעגלים דינמיים. המטרה היא לבצע אופטימיזציה של המעגל להפעלה על חומרה על ידי הפחתת העומק בהשוואה למימוש יוניטרי טהור. כדי להמחיש את היתרונות, נציג את הבנייה הדינמית של LRCX ואת היוניטרי השווה ערך שלו, ומאוחר יותר נשווה את הביצועים שלהם לאחר transpilation. חשוב לציין שבעוד שכאן אנחנו מיישמים את ה-LRCX על בעיה פשוטה מאותחלת ב-Hadamard, הפרוטוקול ניתן ליישום על כל מעגל שבו נדרש CNOT ארוך טווח.
הכנת זוגות Bell
נתחיל ביצירת שרשרת של זוגות Bell לאורך הנתיב בין ה-qubits של הבקרה והמטרה. אם המרחק הוא אי-זוגי, נחיל תחילה CNOT מהבקרה לשכן שלה, שהוא ה-CNOT שיטלפורט. עבור מרחק זוגי, CNOT זה יוחל לאחר שלב הכנת זוגות ה-Bell. שרשרת זוגות ה-Bell משזרת אז זוגות עוקבים של qubits, מקימה את המשאב הדרוש להעברת מידע הבקרה על פני ההתקן.
# Determine where to start the Bell pair chain and add an extra CNOT when n is odd
def check_even(n: int) -> int:
"""Return 1 if n is even, else 2."""
return 1 if n % 2 == 0 else 2
def prepare_bell_pairs(qc, add_barriers=True):
n = qc.num_qubits - 2 # number of qubits between target and control
k = int(n / 2)
if add_barriers:
qc.barrier()
x0 = check_even(n)
if n % 2 != 0:
qc.cx(0, 1)
# Create k Bell pairs
for i in range(k):
qc.h(x0 + 2 * i)
qc.cx(x0 + 2 * i, x0 + 2 * i + 1)
return qc
qc = prepare_bell_pairs(qc)
qc.draw(output="mpl", fold=-1, scale=0.5)
מדידת זוגות qubits שכנים בבסיס Bell
לאחר מכן, נמדוד qubits שכנים לא-משוזרים בבסיס Bell (מדידות דו-qubit של ו-). זה יוצר זוג Bell ארוך טווח בין qubit המטרה וה-qubit הסמוך לבקרה (עד לתיקוני Pauli, שיוטמעו באמצעות הזנה קדימה בשלב הבא). במקביל, נממש את המדידה המשזרת שמטלפורטת את שער CNOT לפעול על qubit המטרה המיועד.
def measure_bell_basis(qc, add_barriers=True):
n = qc.num_qubits - 2 # number of qubits between target and control
k = int(n / 2)
if n > 1:
_, c1, c2 = qc.cregs
elif n > 0:
_, c2 = qc.cregs
# Determine where to start the Bell pair chain and add an extra CNOT
# when n is odd
x0 = 1 if n % 2 == 0 else 2
# Entangling layer that implements the Bell measurement
# (and additionally adds the CNOT to be
# teleported, if n is even)
for i in range(k + 1):
qc.cx(x0 - 1 + 2 * i, x0 + 2 * i)
for i in range(1, k + x0):
if i == 1:
qc.h(2 * i + 1 - x0)
else:
qc.h(2 * i + 1 - x0)
if add_barriers:
qc.barrier()
# Map the ZZ measurements onto classical register c1
for i in range(k):
if i == 0:
qc.measure(2 * i + x0, c1[i])
else:
qc.measure(2 * i + x0, c1[i])
# Map the XX measurements onto classical register c2
for i in range(1, k + x0):
if i == 1:
qc.measure(2 * i + 1 - x0, c2[i - 1])
else:
qc.measure(2 * i + 1 - x0, c2[i - 1])
return qc
qc = measure_bell_basis(qc)
qc.draw(output="mpl", fold=-1, scale=0.5)
החלת תיקוני הזנה קדימה לתיקון אופרטורי תוצר לוואי של Pauli
מדידות בסיס Bell מכניסות תוצרי לוואי של Pauli שיש לתקן באמצעות התוצאות המתועדות. זה נעשה בשני שלבים. ראשית, אנחנו צריכים לחשב את הזוגיות של כל מדידות ה-, שמשמשת אז להחלת מותנית של שער על qubit המטרה. באופן דומה, הזוגיות של מדידות ה- מחושבת ומשמשת להחלת מותנית של שער על qubit הבקרה.
עם מסגרת הביטוי הקלאסי החדשה ב-Qiskit, זוגיות אלה ניתנות לחישוב ישירות בשכבת העיבוד הקלאסית של המעגל. במקום להחיל רצף של שערים מותנים בודדים עבור כל ביט מדידה, נוכל לבנות ביטוי קלאסי יחיד המייצג את ה-XOR (זוגיות) של כל תוצאות המדידה הרלוונטיות. ביטוי זה משמש אז כתנאי בבלוק if_test יחיד, מה שמאפשר להחיל את שערי התיקון בעומק קבוע. גישה זו גם מפשטת את המעגל וגם מבטיחה שתיקוני ההזנה קדימה לא מכניסים עיכוב נוסף מיותר.
def apply_ffwd_corrections(qc):
control = 0 # control qubit
target = qc.num_qubits - 1 # target qubit
n = qc.num_qubits - 2 # number of qubits between target and control
k = int(n / 2)
x0 = check_even(n)
if n > 1:
_, c1, c2 = qc.cregs
elif n > 0:
_, c2 = qc.cregs
# First, let's compute the parity of all ZZ measurements
for i in range(k):
if i == 0:
parity_ZZ = expr.lift(
c1[i]
) # Store the value of the first ZZ measurement in parity_ZZ
else:
parity_ZZ = expr.bit_xor(
c1[i], parity_ZZ
) # Successively compute the parity via XOR operations
for i in range(1, k + x0):
if i == 1:
parity_XX = expr.lift(
c2[i - 1]
) # Store the value of the first XX measurement in parity_XX
else:
parity_XX = expr.bit_xor(
c2[i - 1], parity_XX
) # Successively compute the parity via XOR operations
if n > 0:
with qc.if_test(parity_XX):
qc.z(control)
if n > 1:
with qc.if_test(parity_ZZ):
qc.x(target)
return qc
qc = apply_ffwd_corrections(qc)
qc.draw(output="mpl", fold=-1, scale=0.5)
מדידת qubits הבקרה והמטרה
נגדיר פונקציית עזר המאפשרת מדידה של qubits הבקרה והמטרה בבסיסים , , או . לאימות מצב Bell , ערכי הציפייה של ו- שניהם צריכים להיות , מכיוון שהם מייצבים של המצב. מדידת נתמכת גם כאן ותשמש להלן בעת חישוב הנאמנות.
def measure_in_basis(qc, basis="XX", add_barrier=True):
control = 0 # control qubit
target = qc.num_qubits - 1 # target qubit
assert basis in ["XX", "YY", "ZZ"]
qc = (
qc.copy()
) # We copy the circuit because we want to measure in different bases
cr = qc.cregs[0]
if add_barrier:
qc.barrier()
if basis == "XX":
qc.h(control)
qc.h(target)
elif basis == "YY":
qc.sdg(control)
qc.sdg(target)
qc.h(control)
qc.h(target)
qc.measure(control, cr[0])
qc.measure(target, cr[1])
return qc
qc_YY = measure_in_basis(qc.copy(), basis="YY")
qc_YY.draw(
output="mpl", fold=-1, scale=0.5
) # Circuit for measuring in the YY basis
הרכבת הכל ביחד
נשלב את השלבים השונים שהוגדרו לעיל כדי ליצור שער CX ארוך טווח על שני קצוות של קו חד-ממדי (1D). השלבים כוללים את הבאים:
- אתחול qubit הבקרה ב-
- הכנת זוגות Bell
- מדידת זוגות qubits שכנים
- החלת תיקוני הזנה קדימה התלויים ב-MCMs
def lrcx(distance, prep_barrier=True, pre_measure_barrier=True):
qc = initialize_circuit(distance)
qc = prepare_bell_pairs(qc, prep_barrier)
qc = measure_bell_basis(qc, pre_measure_barrier)
qc = apply_ffwd_corrections(qc)
return qc
qc = lrcx(distance)
# Apply the measurement in the XX, YY, and ZZ bases
qc_XX, qc_YY, qc_ZZ = [
measure_in_basis(qc, basis=basis) for basis in ["XX", "YY", "ZZ"]
]
qc_YY.draw(
output="mpl", fold=-1, scale=0.5
) # Circuit for measuring in the YY basis
מימוש מבוסס יוניטרי המחליף את ה-qubits לאמצע
להשוואה, נבחן תחילה את המקרה שבו שער CNOT ארוך טווח מיושם באמצעות חיבורים בין שכנים קרובים ושערים יוניטריים. באיור הבא, בצד שמאל מעגל לשער CNOT ארוך טווח המשתרע על שרשרת 1D של n-qubits הכפופה לחיבורים בין שכנים קרובים בלבד. באמצע פירוק יוניטרי שווה ערך הניתן למימוש עם שערי CNOT מקומיים, עומק מעגל .

המעגל באמצע יכול להיות מיושם באופן הבא:
def cnot_unitary(distance):
"""Generate a long range CNOT gate using local CNOTs on a 1D
chain of qubits subject to n
nearest-neighbor connections only.
Args:
distance (int) : The distance of the CNOT gate,
with the convention that
a distance of 0 is a nearest-neighbor CNOT.
Returns:
QuantumCircuit: A Quantum Circuit implementing a
long-range CNOT gate
between qubit 0 and qubit distance+1
"""
assert distance >= 0
n = distance # number of qubits between target and control
qr = QuantumRegister(
n + 2, name="q"
) # Circuit with n qubits between control and target
cr = ClassicalRegister(
2, name="cr"
) # Classical register for measuring control and target qubits
qc = QuantumCircuit(qr, cr, name="CNOT_unitary")
control_qubit = 0
qc.h(control_qubit) # Prepare the control qubit in the |+> state
k = int(n / 2)
qc.barrier()
for i in range(control_qubit, control_qubit + k):
qc.cx(i, i + 1)
qc.cx(i + 1, i)
qc.cx(-i - 1, -i - 2)
qc.cx(-i - 2, -i - 1)
if n % 2 == 1:
qc.cx(k + 2, k + 1)
qc.cx(k + 1, k + 2)
qc.barrier()
qc.cx(k, k + 1)
for i in range(control_qubit, control_qubit + k):
qc.cx(k - i, k - 1 - i)
qc.cx(k - 1 - i, k - i)
qc.cx(k + i + 1, k + i + 2)
qc.cx(k + i + 2, k + i + 1)
if n % 2 == 1:
qc.cx(-2, -1)
qc.cx(-1, -2)
return qc
qc_uni = cnot_unitary(distance)
כעת נבנה את המעגלים שמודדים בבסיסים , , ו-, בדיוק כפי שעשינו עבור המעגלים הדינמיים לעיל.
# Apply the measurement in the XX, YY, and ZZ bases
qc_uni_XX, qc_uni_YY, qc_uni_ZZ = [
measure_in_basis(qc_uni, basis=basis) for basis in ["XX", "YY", "ZZ"]
]
qc_uni_YY.draw(
output="mpl", fold=-1, scale=0.5
) # Circuit for measuring in the YY basis
כעת שבנינו הן את המעגלים הדינמיים והן את המעגלים היוניטריים לדוגמה בקנה מידה קטן עם distance=6, נבצע transpile להם כדי להריץ תחילה על סימולטור ללא רעש.
from qiskit_aer import AerSimulator
aer_backend = AerSimulator()
pm_sim = generate_preset_pass_manager(
optimization_level=0, backend=aer_backend
)
# Dynamic circuits
isa_sim_dyn = pm_sim.run([qc_XX, qc_YY, qc_ZZ])
# Unitary circuits
isa_sim_uni = pm_sim.run([qc_uni_XX, qc_uni_YY, qc_uni_ZZ])
שלב 3: ביצוע באמצעות פרימיטיבים של Qiskit
כעת נוכל להריץ את הניסוי על ה-backend של הסימולטור ללא רעש. נשתמש ב-Qiskit Runtime Sampler עם AerSimulator כמצב ה-backend כדי להפעיל את המעגלים.
sampler_sim = Sampler(mode=aer_backend)
sim_job = sampler_sim.run(isa_sim_dyn + isa_sim_uni)
sim_results = sim_job.result()
שלב 4: עיבוד לאחר והחזרת תוצאה בפורמט קלאסי רצוי
לאחר שהניסויים הופעלו בהצלחה, כעת נעבד את ספירות המדידה כדי לחלץ מדדים משמעותיים. בשלב זה, אנחנו עושים את הבאים:
- מגדירים מדדי איכות להערכת הביצועים של ה-CX ארוך הטווח.
- מחשבים ערכי ציפייה של אופרטורי Pauli מתוצאות מדידה גולמיות.
- משתמשים בהם כדי לחשב את נאמנות מצב ה-Bell שנוצר.
בסימולציה ללא רעש, נוודא שמדד הנאמנות הוא עבור המעגלים שנבנו. בניסויים על ה-QPUs האמיתיים, ניתוח זה יספק תמונה ברורה של עד כמה המעגלים הדינמיים מתפקדים בהשוואה למימוש היוניטרי הבסיסי.
מדדי איכות
כדי להעריך את ההצלחה של פרוטוקול CX ארוך הטווח, אנחנו מודדים עד כמה מצב הפלט קרוב למצב Bell האידיאלי. דרך נוחה לכמת זאת היא על ידי חישוב נאמנות המצב באמצעות ערכי ציפייה של אופרטורי Pauli. נאמנות למצב Bell על מצב הבקרה והמטרה ניתן לחישוב לאחר ידיעת , , ו-. באופן ספציפי,
כדי לחשב את ערכי הציפייה הללו מנתוני מדידה גולמיים, נגדיר קבוצה של פונקציות עזר:
compute_ZZ_expectation: בהינתן ספירות מדידה, מחשבת את ערך הציפייה של אופרטור Pauli דו-qubit בבסיס .compute_fidelity: משלבת את ערכי הציפייה של , , ו- לביטוי הנאמנות לעיל.get_counts_from_bitarray: כלי עזר לחילוץ ספירות מאובייקטי תוצאות backend.
def compute_ZZ_expectation(counts):
total = sum(counts.values())
expectation = 0
for bitstring, count in counts.items():
# Ensure bitstring is 2 bits
z1 = (-1) ** (int(bitstring[-1]))
z2 = (-1) ** (int(bitstring[-2]))
expectation += z1 * z2 * count
return expectation / total
def compute_fidelity(counts_xx, counts_yy, counts_zz):
xx, yy, zz = [
compute_ZZ_expectation(c) for c in [counts_xx, counts_yy, counts_zz]
]
return 1 / 4 * (1 + xx - yy + zz)
# Dynamic fidelity
counts_xx = sim_results[0].data.cr.get_counts()
counts_yy = sim_results[1].data.cr.get_counts()
counts_zz = sim_results[2].data.cr.get_counts()
fidelity_dyn = compute_fidelity(counts_xx, counts_yy, counts_zz)
# Unitary fidelity
counts_xx = sim_results[3].data.cr.get_counts()
counts_yy = sim_results[4].data.cr.get_counts()
counts_zz = sim_results[5].data.cr.get_counts()
fidelity_uni = compute_fidelity(counts_xx, counts_yy, counts_zz)
print(f"Dynamic fidelity (distance={distance}): {fidelity_dyn:.4f}")
print(f"Unitary fidelity (distance={distance}): {fidelity_uni:.4f}")
Dynamic fidelity (distance=6): 1.0000
Unitary fidelity (distance=6): 1.0000
כצפוי בסימולציה ללא רעש, הנאמנויות במעגלים הדינמיים וביוניטריים הן .
דוגמת חומרה בקנה מידה גדול
כאן נרכיב את כל הפרטים הללו לתהליך עבודה אחד בקנה מידה גדול יותר, שמופעל לאחר מכן על חומרה קוונטית אמיתית.
יצירת מעגלים למרחקים שונים
כעת ניצור מעגלי CX ארוכי טווח עבור טווח של הפרדות qubit עד 60 qubits. עבור כל מרחק, נבנה מעגלים שמודדים בבסיסים , , ו-, שישמשו מאוחר יותר לחישוב נאמנויות.
רשימת המרחקים כוללת הפרדות קצרות וארוכות טווח, כאשר distance = 0 מתאים ל-CX שכן-קרוב. אותם מרחקים ישמשו גם ליצירת המעגלים היוניטריים המתאימים מאוחר יותר להשוואה.
# -------------------------Step 1-------------------------
distances = [
0,
1,
2,
3,
6,
11,
16,
21,
28,
35,
44,
55,
60,
] # Distances for long range CX. distance of 0 is a nearest-neighbor CX
distances.sort()
assert min(distances) >= 0
basis_list = ["XX", "YY", "ZZ"]
# Dynamic circuits
circuits_dyn = []
for distance in distances:
for basis in basis_list:
circuits_dyn.append(
measure_in_basis(lrcx(distance, prep_barrier=False), basis=basis)
)
print(f"Number of circuits: {len(circuits_dyn)}")
# Unitary circuits
circuits_uni = []
for distance in distances:
for basis in basis_list:
circuits_uni.append(
measure_in_basis(cnot_unitary(distance), basis=basis)
)
print(f"Number of circuits: {len(circuits_uni)}")
# -------------------------Step 2-------------------------
# Set up access to IBM Quantum devices
from qiskit.circuit import IfElseOp
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=156
)
if "if_else" not in backend.target.operation_names:
backend.target.add_instruction(IfElseOp, name="if_else")
שימוש במחרוזת Layer Fidelity לבחירת שרשרת חד-ממדית
מכיוון שאנחנו רוצים להשוות את הביצועים של מעגלים דינמיים ויוניטריים על שרשרת חד-ממדית, אנחנו משתמשים במחרוזת Layer Fidelity כדי לבחור טופולוגיה לינארית של שרשרת ה-qubits הטובה ביותר מהמכשיר. זה מבטיח ששני סוגי המעגלים עוברים טרנספילציה תחת אותן מגבלות קישוריות, מה שמאפשר השוואה הוגנת של ביצועיהם.
# This selects best qubits for longest distance and uses
# the same control for all lengths
lf_qubits = backend.properties().to_dict()[
"general_qlists"
] # best linear chain qubits
chosen_layouts = {
distance: [
val["qubits"]
for val in lf_qubits
if val["name"] == f"lf_{distances[-1] + 2}"
][0][: distance + 2]
for distance in distances
}
print(chosen_layouts[max(distances)]) # best qubits at each distance
[11, 12, 13, 14, 15, 19, 35, 34, 33, 39, 53, 54, 55, 59, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 76, 81, 82, 83, 84, 85, 86, 87, 97, 107, 108, 109, 110, 111, 98, 91, 92, 93, 94, 95, 99, 115, 114, 113, 119, 133, 132, 131, 138, 151, 150, 149, 148]
isa_circuits_dyn = []
isa_circuits_uni = []
# Using the same initial layouts for both circuits for better
# apples to apples comparison
for qc in circuits_dyn:
pm = generate_preset_pass_manager(
optimization_level=1,
backend=backend,
initial_layout=chosen_layouts[qc.num_qubits - 2],
)
isa_circuits_dyn.append(pm.run(qc))
for qc in circuits_uni:
pm = generate_preset_pass_manager(
optimization_level=1,
backend=backend,
initial_layout=chosen_layouts[qc.num_qubits - 2],
)
isa_circuits_uni.append(pm.run(qc))
print(
f"2Q depth: "
f"{isa_circuits_dyn[14].depth(lambda x: x.operation.num_qubits == 2)}"
)
isa_circuits_dyn[14].draw("mpl", fold=-1, idle_wires=0)
2Q depth: 2

print(
f"2Q depth: "
f"{isa_circuits_uni[14].depth(lambda x: x.operation.num_qubits == 2)}"
)
isa_circuits_uni[14].draw("mpl", fold=-1, idle_wires=False)
2Q depth: 13

הצגה חזותית של qubits המשמשים למעגל LRCX
בסעיף זה, נבחן כיצד מעגל LRCX ממופה על החומרה. נתחיל בהצגה חזותית של ה-qubits הפיזיים המשמשים במעגל ואז נלמד כיצד מרחק הבקרה-מטרה בפריסה משפיע על מספר הפעולות.
# Note: the qubit coordinates must be hard-coded.
# The backend API does not currently provide this information directly.
# If using a different backend, you will need to
# adjust the coordinates accordingly,
# or set the qubit_coordinates = None to use the default layout coordinates.
def _heron_coords_r2():
"""Generate coordinates for the Heron layout in R2. Note"""
cord_map = np.array(
[
[
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
3,
7,
11,
15,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
1,
5,
9,
13,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
3,
7,
11,
15,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
1,
5,
9,
13,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
3,
7,
11,
15,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
1,
5,
9,
13,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
3,
7,
11,
15,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
],
-1
* np.array([j for i in range(15) for j in [i] * [16, 4][i % 2]]),
],
dtype=int,
)
hcords = []
ycords = cord_map[0]
xcords = cord_map[1]
for i in range(156):
hcords.append([xcords[i] + 1, np.abs(ycords[i]) + 1])
return hcords
# Visualize the active qubits in the circuit layout
plot_circuit_layout(
circuit=isa_circuits_uni[-1],
backend=backend,
view="physical",
qubit_coordinates=_heron_coords_r2(),
)

לאחר מכן, נבצע את הניסוי על ה-backend האמיתי. אנחנו גם עושים שימוש באצווה כדי להפעיל ביעילות את הניסוי על פני ניסיונות מרובים. הרצת ניסיונות חוזרים מאפשרת לנו לחשב ממוצעים להשוואה מדויקת יותר בין השיטות היוניטרית והדינמית, כמו גם לכמת את השונות שלהן על ידי השוואת הסטיות בין הריצות.
# -------------------------Step 3-------------------------
num_trials = 10
jobs_uni = []
jobs_dyn = []
with Batch(backend=backend) as batch:
sampler = Sampler(mode=batch)
sampler.options.environment.job_tags = ["TUT_LRE"]
for _ in range(num_trials):
jobs_uni.append(sampler.run(isa_circuits_uni, shots=1024))
jobs_dyn.append(sampler.run(isa_circuits_dyn, shots=1024))
נחשב את הנאמנות עבור מעגלי CX ארוכי הטווח הדינמיים. עבור כל מרחק, נחלץ תוצאות מדידה בבסיסים , , ו-. תוצאות אלה משולבות באמצעות פונקציות העזר שהוגדרו קודם לחישוב הנאמנות לפי . זה מספק את הנאמנות הנצפית של הפרוטוקול המבוצע באופן דינמי בכל מרחק.
# -------------------------Step 4-------------------------
fidelities_dyn = []
# loop over trials
for job in jobs_dyn:
result_dyn = job.result()
trial_fidelities = []
# loop over all distances
for ind, dist in enumerate(distances):
counts_xx = result_dyn[ind * 3].data.cr.get_counts()
counts_yy = result_dyn[ind * 3 + 1].data.cr.get_counts()
counts_zz = result_dyn[ind * 3 + 2].data.cr.get_counts()
trial_fidelities.append(
compute_fidelity(counts_xx, counts_yy, counts_zz)
)
fidelities_dyn.append(trial_fidelities)
# average over trials for each distance
avg_fidelities_dyn = np.mean(fidelities_dyn, axis=0)
std_fidelities_dyn = np.std(fidelities_dyn, axis=0)
כעת נחשב את הנאמנות עבור מעגלי CX ארוכי הטווח היוניטריים, ונעשה זאת באותו אופן כפי שעשינו עבור המעגלים הדינמיים לעיל.
fidelities_uni = []
# loop over trials
for job in jobs_uni:
result_uni = job.result()
trial_fidelities = []
# loop over all distances
for ind, dist in enumerate(distances):
counts_xx = result_uni[ind * 3].data.cr.get_counts()
counts_yy = result_uni[ind * 3 + 1].data.cr.get_counts()
counts_zz = result_uni[ind * 3 + 2].data.cr.get_counts()
trial_fidelities.append(
compute_fidelity(counts_xx, counts_yy, counts_zz)
)
fidelities_uni.append(trial_fidelities)
# average over trials for each distance
avg_fidelities_uni = np.mean(fidelities_uni, axis=0)
std_fidelities_uni = np.std(fidelities_uni, axis=0)
הצגת התוצאות
כדי להעריך את התוצאות באופן ויזואלי, התא שלהלן מציג את נאמנויות השער המוערכות שנמדדו במרחקים משתנים בין qubits משוזרים עבור כל שיטה.
fig, ax = plt.subplots()
# Unitary with error bars
ax.errorbar(
distances,
avg_fidelities_uni,
yerr=std_fidelities_uni,
fmt="o-.",
color="c",
ecolor="c",
elinewidth=1,
capsize=4,
label="Unitary",
)
# Dynamic with error bars
ax.errorbar(
distances,
avg_fidelities_dyn,
yerr=std_fidelities_dyn,
fmt="o-.",
color="m",
ecolor="m",
elinewidth=1,
capsize=4,
label="Dynamic",
)
# Random gate baseline
ax.axhline(y=1 / 4, linestyle="--", color="gray", label="Random gate")
legend = ax.legend(frameon=True)
for text in legend.get_texts():
text.set_color("black")
legend.get_frame().set_facecolor("white")
legend.get_frame().set_edgecolor("black")
ax.set_title(
"Bell State Fidelity vs Control–Target Separation", color="black"
)
ax.set_xlabel("Distance", color="black")
ax.set_ylabel("Bell state fidelity", color="black")
ax.grid(linestyle=":", linewidth=0.6, alpha=0.4, color="gray")
ax.set_ylim((0.2, 1))
ax.set_facecolor("white")
fig.patch.set_facecolor("white")
for spine in ax.spines.values():
spine.set_visible(True)
spine.set_color("black")
ax.tick_params(axis="x", colors="black")
ax.tick_params(axis="y", colors="black")
plt.show()

מגרף הנאמנות לעיל, ה-LRCX לא עלה בעקביות על המימוש היוניטרי הישיר. למעשה, עבור הפרדות בקרה-מטרה קצרות, המעגל היוניטרי השיג נאמנות גבוהה יותר. עם זאת, בהפרדות גדולות יותר, המעגל הדינמי מתחיל להשיג נאמנות טובה יותר מהמימוש היוניטרי. התנהגות זו אינה בלתי צפויה בחומרה נוכחית: בעוד שמעגלים דינמיים מפחיתים את עומק המעגל על ידי הימנעות משרשראות SWAP ארוכות, הם מכניסים זמן מעגל נוסף ממדידות בתוך המעגל, הזנה קדימה קלאסית ועיכובי נתיב בקרה. האיחור הנוסף מגביר את הדה-קוהרנטיות ושגיאות הקריאה, שיכולות לעלות על החיסכון בעומק במרחקים קצרים.
עם זאת, אנחנו צופים בנקודת מעבר שבה הגישה הדינמית עולה על היוניטרית. זוהי תוצאה ישירה של הסקלה השונה: העומק של המעגל היוניטרי גדל באופן לינארי עם המרחק בין qubits, בעוד שעומק המעגל הדינמי נשאר קבוע.
נקודות מרכזיות:
- יתרון מיידי של מעגלים דינמיים: המוטיבציה העיקרית כיום היא הפחתת עומק דו-qubit, לא בהכרח שיפור נאמנות.
- מדוע נאמנות יכולה להיות גרועה יותר היום: זמן מעגל מוגבר ממדידה ופעולות קלאסיות לעתים קרובות שולט, במיוחד כאשר הפרדת הבקרה-מטרה קטנה.
- מבט קדימה: ככל שהחומרה משתפרת, במיוחד קריאה מהירה יותר, איחור בקרה קלאסי קצר יותר ותקורת מעגל-ביניים מופחתת, אנחנו צריכים לצפות שהפחתות העומק והמשך הללו יתורגמו לשיפורי נאמנות מדידים.
# Compute metrics for each distance, skipping the basis circuits since
# they are identical for each distance
depths_2q_dyn = [
c.depth(lambda x: x.operation.num_qubits == 2)
for c in isa_circuits_dyn[::3]
]
meas_dyn = [
sum(1 for instr in c.data if instr.operation.name == "measure")
for c in isa_circuits_dyn[::3]
]
depths_2q_uni = [
c.depth(lambda x: x.operation.num_qubits == 2)
for c in isa_circuits_uni[::3]
]
meas_uni = [
sum(1 for instr in c.data if instr.operation.name == "measure")
for c in isa_circuits_uni[::3]
]
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
axes[0].plot(
distances, depths_2q_uni, "o-.", color="c", label="Unitary (2Q depth)"
)
axes[0].plot(
distances, depths_2q_dyn, "o-.", color="m", label="Dynamic (2Q depth)"
)
axes[0].set_xlabel("Number of qubits between control and target")
axes[0].set_ylabel("Two-qubit depth")
axes[0].grid(True, linestyle=":", linewidth=0.6, alpha=0.4)
axes[0].legend()
axes[1].plot(
distances, meas_uni, "o-.", color="c", label="Unitary (# measurements)"
)
axes[1].plot(
distances, meas_dyn, "o-.", color="m", label="Dynamic (# measurements)"
)
axes[1].set_xlabel("Number of qubits between control and target")
axes[1].set_ylabel("Number of measurements")
axes[1].grid(True, linestyle=":", linewidth=0.6, alpha=0.4)
axes[1].legend()
fig.suptitle("Scaling of Unitary vs Dynamic LRCX with Distance", fontsize=12)
plt.tight_layout()
plt.show()

גרף עומק דו-qubit זה מדגיש את היתרון העיקרי של LRCX המיושם עם מעגלים דינמיים: הביצועים נשארים בעצם קבועים ככל שההפרדה בין qubits הבקרה והמטרה גדלה. לעומת זאת, המימוש היוניטרי גדל באופן לינארי עם המרחק עקב שרשראות ה-SWAP הנדרשות. העומק לוכד את הסקלה הלוגית של פעולות דו-qubit, בעוד שספירת המדידה משקפת את התקורה הנוספת עבור מעגלים דינמיים. מדידות אלה יעילות, מכיוון שהן מתבצעות במקביל, אך הן עדיין מכניסות עלות קבועה בחומרה של היום.
מדוע נאמנות יכולה להיות גרועה יותר היום: זמן מעגל מוגבר ממדידה ופעולות קלאסיות לעתים קרובות שולט, במיוחד כאשר הפרדת הבקרה-מטרה קטנה. לדוגמה, אורך הקריאה הממוצע במעבד Heron r2 הוא 2,280 ns, בעוד שאורך שער 2Q שלו הוא רק 68 ns.
ככל שאיחורי המדידה והקלאסיים משתפרים, אנחנו צופים שהסקלה של עומק קבוע ומדידות קבועות של מעגלים דינמיים תניב יתרונות נאמנות וזמן ריצה ברורים במעגלים גדולים יותר.
צעדים הבאים
אם מצאת עבודה זו מעניינת, ייתכן שתתעניין בחומרים הבאים:
הפניות
[1] Efficient Long-Range Entanglement using Dynamic Circuits, by Elisa Bäumer, Vinay Tripathi, Derek S. Wang, Patrick Rall, Edward H. Chen, Swarnadeep Majumder, Alireza Seif, Zlatko K. Minev. IBM Quantum, (2023). https://arxiv.org/abs/2308.13065