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

תחילת העבודה עם חיתוך מעגלים באמצעות חיתוכי Wire

גרסאות חבילות

הקוד בדף זה פותח עם הדרישות הבאות. מומלץ להשתמש בגרסאות אלה או בגרסאות חדשות יותר.

qiskit[all]~=2.3.0
qiskit-ibm-runtime~=0.43.1
qiskit-aer~=0.17
qiskit-addon-cutting~=0.10.0

מדריך זה מדגים דוגמת עבודה של חיתוכי Wire עם חבילת qiskit-addon-cutting. הוא מכסה בנייה מחדש של ערכי ההציפייה של מעגל בן שבעה Qubit-ים באמצעות חיתוך Wire.

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

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

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

הדוגמה הבאה מדגימה בנייה מחדש של ערך ההציפייה לאחר חיתוך Wire. תיצור מעגל עם מספר Gate-ים לא-מקומיים ותגדיר observable-ים לאמידה.

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-cutting qiskit-aer qiskit-ibm-runtime
import numpy as np
from qiskit import QuantumCircuit
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.quantum_info import SparsePauliOp
from qiskit_ibm_runtime.fake_provider import FakeManilaV2
from qiskit_ibm_runtime import SamplerV2, Batch
from qiskit_aer.primitives import EstimatorV2

from qiskit_addon_cutting.instructions import Move, CutWire
from qiskit_addon_cutting import (
partition_problem,
generate_cutting_experiments,
cut_wires,
expand_observables,
reconstruct_expectation_values,
)

qc_0 = QuantumCircuit(7)
for i in range(7):
qc_0.rx(np.pi / 4, i)
qc_0.cx(0, 3)
qc_0.cx(1, 3)
qc_0.cx(2, 3)
qc_0.cx(3, 4)
qc_0.cx(3, 5)
qc_0.cx(3, 6)
qc_0.cx(0, 3)
qc_0.cx(1, 3)
qc_0.cx(2, 3)

# Define observable
observable = SparsePauliOp(["ZIIIIII", "IIIZIII", "IIIIIIZ"])

# Draw circuit
qc_0.draw("mpl")

Output of the previous code cell

חיתוך Wire באמצעות הוראת CutWire ברמה גבוהה

בשלב הבא, בצע חיתוכי Wire באמצעות הוראת CutWire החד-Qubit על Qubit q3q_3. לאחר שתת-הניסויים מוכנים לביצוע, השתמש בפונקציה cut_wires() כדי להמיר הוראות CutWire להוראות Move על Qubit-ים שהוקצו לאחרונה.

qc_1 = QuantumCircuit(7)
for i in range(7):
qc_1.rx(np.pi / 4, i)
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.append(CutWire(), [3])
qc_1.cx(3, 4)
qc_1.cx(3, 5)
qc_1.cx(3, 6)
qc_1.append(CutWire(), [3])
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)

qc_1.draw("mpl")

Output of the previous code cell

הערה לגבי הרחבת observable-ים

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

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

# Transform CutWire instructions to Move instructions
qc_2 = cut_wires(qc_1)

# Expand the observable to match the new circuit size
expanded_observable = expand_observables(observable.paulis, qc_0, qc_2)
print(f"Expanded Observable: {expanded_observable}")
qc_2.draw("mpl")
Expanded Observable: ['ZIIIIIIII', 'IIIZIIIII', 'IIIIIIIIZ']

Output of the previous code cell

חלוקת המעגל וה-observable

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

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

partitioned_problem = partition_problem(
circuit=qc_2,
observables=expanded_observable,
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases

print(f"Subobservables to measure: \n{subobservables}\n")
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
subcircuits[0].draw("mpl")
Subobservables to measure:
{0: PauliList(['IIIII', 'ZIIII', 'IIIIZ']), 1: PauliList(['ZIII', 'IIII', 'IIII'])}

Sampling overhead: 256.0

Output of the previous code cell

subcircuits[1].draw("mpl")

Output of the previous code cell

בסכמת חלוקה זו, חתכת שני Wire-ים, מה שגורם לתקורת דגימה של 444^4.

יצירת תת-ניסויים לביצוע ועיבוד לאחר תוצאות

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

הערה לגבי מספר הדגימות

הארגומנט num_samples מציין כמה דגימות לשלוף מהתפלגות הסבירות המדומה ומשפיע על דיוק המקדמים המשמשים לבנייה מחדש. העברת אינסוף (np.inf) מבטיחה שכל המקדמים יחושבו במדויק. קרא את תיעוד ה-API על יצירת משקלים ויצירת ניסויי חיתוך למידע נוסף.

# Generate subexperiments
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits, observables=subobservables, num_samples=np.inf
)

# Set a backend to use and transpile the subexperiments
backend = FakeManilaV2()
pass_manager = generate_preset_pass_manager(
optimization_level=1, backend=backend
)
isa_subexperiments = {
label: pass_manager.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}

# Submit each partition's subexperiments to the Qiskit Runtime Sampler
# primitive, in a single batch so that the jobs will run back-to-back.
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}

לבסוף, ניתן לבנות מחדש את ערך ההציפייה של המעגל המלא באמצעות המתודה reconstruct_expectation_values().

בלוק הקוד הבא בונה מחדש את התוצאות ומשווה אותן לערך ההציפייה המדויק.

reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
# Apply the coefficients of the original observable
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)

# Compute the exact expectation value using the `qiskit_aer` package.
estimator = EstimatorV2()
exact_expval = estimator.run([(qc_0, observable)]).result()[0].data.evs
print(
f"Reconstructed expectation value: {np.real(np.round(reconstructed_expval, 8))}"
)
print(f"Exact expectation value: {np.round(exact_expval, 8)}")
print(
f"Error in estimation: {np.real(np.round(reconstructed_expval-exact_expval, 8))}"
)
print(
f"Relative error in estimation: {np.real(np.round((reconstructed_expval-exact_expval) / exact_expval, 8))}"
)
Reconstructed expectation value: 1.45965266
Exact expectation value: 1.59099026
Error in estimation: -0.1313376
Relative error in estimation: -0.08255085
הערה לגבי מקדמי ה-observable

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

בדרך כלל ניתן להחיל מקדמים אלה באמצעות numpy.dot() כפי שהוצג בעבר.

חיתוך Wire באמצעות הוראת Move ברמה נמוכה

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

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

qc_1 = QuantumCircuit(8)
for i in [*range(4), *range(5, 8)]:
qc_1.rx(np.pi / 4, i)
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.append(Move(), [3, 4])
qc_1.cx(4, 5)
qc_1.cx(4, 6)
qc_1.cx(4, 7)
qc_1.append(Move(), [4, 3])
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)

# Expand observable
observable_expanded = SparsePauliOp(["ZIIIIIII", "IIIIZIII", "IIIIIIIZ"])
qc_1.draw("mpl")

Output of the previous code cell

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

partitioned_problem = partition_problem(
circuit=qc_1,
partition_labels="AAAABBBB",
observables=observable_expanded.paulis,
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases

print(f"Subobservables to measure: \n{subobservables}\n")
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
subcircuits["A"].draw("mpl")
Subobservables to measure:
{'A': PauliList(['IIII', 'ZIII', 'IIIZ']), 'B': PauliList(['ZIII', 'IIII', 'IIII'])}

Sampling overhead: 256.0

Output of the previous code cell

subcircuits["B"].draw("mpl")

Output of the previous code cell

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

צעדים הבאים

המלצות