חיתוך חוטים לאומדן ערכי תוחלת
אומדן שימוש: דקה אחת על מעבד Eagle (הערה: זהו אומדן בלבד. זמן הריצה שלך עשוי להשתנות.)
רקע
Circuit-knitting הוא מונח גג המקיף שיטות שונות לחלוקת מעגל למספר מעגלי משנה קטנים יותר הכוללים פחות שערים ו/או קיוביטים. כל אחד ממעגלי המשנה יכול להתבצע באופן עצמאי והתוצאה הסופית מתקבלת על ידי עיבוד קלאסי מסוים על תוצאת כל מעגל משנה. טכניקה זו נגישה ב-circuit cutting Qiskit addon, הסבר מפורט של הטכניקה ניתן ב-docs יחד עם חומר מבוא נוסף.
מחברת זו עוסקת בשיטה הנקראת חיתוך חוטים שבה המעגל מחולק לאורך החוט [1], [2]. שימו לב שחלוקה פשוטה במעגלים קלאסיים מכיוון שהתוצאה בנקודת החלוקה ניתנת לקביעה דטרמיניסטית, והיא 0 או 1. עם זאת, מצב הקיוביט בנקודת החיתוך הוא, באופן כללי, מצב מעורב. לכן, יש למדוד כל מעגל משנה מספר פעמים בבסיסים שונים (בדרך כלל קבוצה שלמה טומוגרפית של בסיסים כגון בסיס Pauli [3], [4] ובהתאם להכין אותו במצב העצמי שלו. האיור למטה (באדיבות: עבודת דוקטורט, Ritajit Majumdar) מציג דוגמה של חיתוך חוטים למצב GHZ של 4 קיוביטים לשלושה מעגלי משנה. כאן מציין קבוצת בסיסים (בדרך כלל Pauli X, Y ו-Z) ו- מציין קבוצת מצבים עצמיים (בדרך כלל , , ו-).

מכיוון שלכל מעגל משנה יש פחות קיוביטים ו/או שערים, הם צפויים להיות פחות רגישים לרעש. מחברת זו מציגה דוגמה שבה ניתן להשתמש בשיטה זו לדיכוי יעיל של הרעש במערכת.
דרישות
לפני שמתחילים את המדריך הזה, ודאו שהדברים הבאים מותקנים:
- Qiskit SDK v2.0 ומעלה, עם תמיכה ב-visualization
- Qiskit Runtime v0.22 ומעלה (
pip install qiskit-ibm-runtime) - Circuit cutting Qiskit addon v0.9.0 ומעלה (
pip install qiskit-addon-cutting)
נשקול מעגל Many Body Localization (MBL) עבור מחברת זו. מעגל MBL הוא מעגל יעיל לחומרה והוא מפורמט על ידי שני פרמטרים ו-. כאשר מוגדר ל- והמצב ההתחלתי מוכן ב- עבור כל הקיוביטים, ערך התוחלת האידיאלי של הוא עבור כל אתר קיוביט ללא תלות בערכים של . ניתן לבדוק פרטים נוספים על מעגלי MBL ב-מאמר זה.
הגדרות
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-cutting qiskit-ibm-runtime
import numpy as np
import matplotlib.pyplot as plt
from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit
from qiskit.quantum_info import PauliList, SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.result import sampled_expectation_value
from qiskit_addon_cutting.instructions import CutWire
from qiskit_addon_cutting import (
cut_wires,
expand_observables,
partition_problem,
generate_cutting_experiments,
reconstruct_expectation_values,
)
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2, Batch
class MBLChainCircuit(QuantumCircuit):
def __init__(
self, num_qubits: int, depth: int, use_cut: bool = False
) -> None:
super().__init__(
num_qubits, name=f"MBLChainCircuit<{num_qubits}, {depth}>"
)
evolution = MBLChainEvolution(num_qubits, depth, use_cut)
self.compose(evolution, inplace=True)
class MBLChainEvolution(QuantumCircuit):
def __init__(self, num_qubits: int, depth: int, use_cut) -> None:
super().__init__(
num_qubits, name=f"MBLChainEvolution<{num_qubits}, {depth}>"
)
theta = Parameter("θ")
phis = ParameterVector("φ", num_qubits)
for layer in range(depth):
layer_parity = layer % 2
# print("layer parity", layer_parity)
for qubit in range(layer_parity, num_qubits - 1, 2):
# print(qubit)
self.cz(qubit, qubit + 1)
self.u(theta, 0, np.pi, qubit)
self.u(theta, 0, np.pi, qubit + 1)
if (
use_cut
and layer_parity == 0
and (
qubit == num_qubits // 2 - 1
or qubit == num_qubits // 2
)
):
self.append(CutWire(), [num_qubits // 2])
if use_cut and layer < depth - 1 and layer_parity == 1:
if qubit == num_qubits // 2:
self.append(CutWire(), [qubit])
for qubit in range(num_qubits):
self.p(phis[qubit], qubit)
חלק I. דוגמה בקנה מידה קטן
שלב 1: מיפוי קלטים קלאסיים לבעיה קוונטית
בתחילה אנחנו בונים מעגל תבנית ללא ערכי פרמטר ספציפיים. אנחנו גם מספקים מקומות שומרי מקום, הנקראים CutWire, כדי לסמן את מיקום החיתוכים. עבור הדוגמה בקנה מידה קטן אנחנו שוקלים מעגל MBL של 10 קיוביטים.
num_qubits = 10
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)
mbl.draw("mpl", fold=-1)

נזכיר שאנו שואפים למצוא את ערך התוחלת של ההמילטוניאן הנצפה כאשר . נשים כמה ערכים אקראיים עבור הפרמטר .
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis
params
[0,
0.2376615174332788,
0.28244289857682414,
0.019248960591717768,
0.46140600996102477,
0.31408025180068433,
0.718184005135733,
0.991153920182475,
0.09289485768301442,
0.8857848280067783,
0.6177529765767047]
כעת אנחנו מסמנים את המעגל לחיתוך על ידי הוספת CutWire מתאים ליצירת שני חיתוכים שווים בערך. אנחנו מגדירים use_cut=True בפונקציה, ומאפשרים לה לסמן אחרי קיוביטים, כאשר הוא מספר הקיוביטים במעגל המקורי.
mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_cut.draw("mpl", fold=-1)

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

Addon זה משתמש בפונקציה cut_wires כדי לתת מענה לקיוביטים הנוספים הנוצרים כתוצאה מהחיתוך.
mbl_move = cut_wires(mbl_cut)
יצירה והרחבת הנצפים
כעת אנחנו בונים את הנצפה . מכיוון שהתוצאה האידיאלית של עבור כל היא , התוצאה האידיאלית של היא גם .
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
observable
PauliList(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII',
'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII',
'IIIIIIIIZI', 'IIIIIIIIIZ'])
עם זאת, שימו לב שמספר הקיוביטים במעגל גדל לאחר הוספת פעולות Move וירטואליות של 2 קיוביטים לאחר החיתוך. לכן, עלינו להרחיב גם את הנצפים על ידי הוספת זהויות כדי להתאים למעגל הנוכחי.
new_obs = expand_observables(observable, mbl, mbl_move)
new_obs
PauliList(['ZIIIIIIIIII', 'IZIIIIIIIII', 'IIZIIIIIIII', 'IIIZIIIIIII',
'IIIIZIIIIII', 'IIIIIIZIIII', 'IIIIIIIZIII', 'IIIIIIIIZII',
'IIIIIIIIIZI', 'IIIIIIIIIIZ'])
שימו לב שכל נצפה התרחב כעת כדי להכיל שבעה קיוביטים, כמו במעגל עם פעולת Move, במקום 6 הקיוביטים המקוריים. לאחר מכן, חלקו את המעגל לשני מעגלי משנה.
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
הבה נצפה במעגלי המשנה
subcircuits = partitioned_problem.subcircuits
subcircuits[0].draw("mpl", fold=-1)

subcircuits[1].draw("mpl", fold=-1)

הנצפים חולקו גם כן כדי להתאים למעגלי המשנה
subobservables = partitioned_problem.subobservables
subobservables
{0: PauliList(['IIIIII', 'IIIIII', 'IIIIII', 'IIIIII', 'IIIIII', 'IZIIII',
'IIZIII', 'IIIZII', 'IIIIZI', 'IIIIIZ']),
1: PauliList(['ZIIII', 'IZIII', 'IIZII', 'IIIZI', 'IIIIZ', 'IIIII', 'IIIII',
'IIIII', 'IIIII', 'IIIII'])}
שימו לב שכל מעגל משנה מוביל למספר דגימות. השחזור לוקח בחשבון את התוצאה של כל אחת מהדגימות הללו. כל אחת מהדגימות הללו נקראת subexperiment.
הרחבת הנצפה באמצעות פעולת Move דורשת מבנה נתונים PauliList. אנחנו יכולים גם ליצור את הנצפה במבנה הנתונים הכללי יותר SparsePauliOp שיהיה שימושי מאוחר יותר במהלך שחזור ה-subexperiments.
M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)
M_z
SparsePauliOp(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII', 'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII', 'IIIIIIIIZI', 'IIIIIIIIIZ'],
coeffs=[0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j,
0.1+0.j, 0.1+0.j])
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)
הבה נראה שתי דוגמאות שבהן הקיוביטים החתוכים נמדדים בשני בסיסים שונים. ראשית, הוא נמדד בבסיס Z רגיל, ולאחר מכן הוא נמדד בבסיס X.
subexperiments[0][6].draw("mpl", fold=-1)

subexperiments[0][2].draw("mpl", fold=-1)

העברת כל subexperiment להתאמה לחומרה
כרגע עלינו להעביר את המעגלים שלנו להתאמה לחומרה לפני הגשתם לביצוע. לכן, נעביר כל מעגל ב-subexperiments תחילה להתאמה לחומרה.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
כעת עלינו להעביר כל אחד מהמעגלים ב-subexperiments להתאמה לחומרה. לשם כך אנחנו יוצרים תחילה מנהל מעברים, ולאחר מכן משתמשים בו כדי להעביר כל אחד מהמעגלים להתאמה לחומרה.
pm = generate_preset_pass_manager(optimization_level=2, backend=backend)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
isa_subexperiments[0][0].draw("mpl", fold=-1, idle_wires=False)
