הפצת אופרטורים אחורה (OBP) לאומדן ערכי תוחלת
אומדן שימוש: 16 דקות על מעבד Eagle r3 (ה ערה: זהו אומדן בלבד. זמן הריצה שלך עשוי להשתנות.)
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-obp qiskit-addon-utils qiskit-ibm-runtime rustworkx
# This cell is hidden from users;
# it disables linting rules.
# ruff: noqa
רקע
הפצת אופרטורים אחורה היא טכניקה הכוללת ספיגת פעולות מסוף מעגל קוונטי לתוך האובזרבבל הנמדד, בדרך כלל מקטינה את העומק של המעגל במחיר איברים נוספים באובזרבבל. המטרה היא להפיץ אחורה כמה שיותר מהמעגל מבלי לאפשר לאובזרבבל לגדול יותר מדי. יישום מבוסס Qiskit זמין בתוסף OBP Qiskit, פרטים נוספים ניתן למצוא בתיעוד המתאים עם דוגמה פשוטה להתחלה.
נבחן דוגמת מעגל שעבורו יש למדוד אובזרבבל , כאשר הם אופרטורי Pauli ו- הם מקדמים. נסמן את המעגל כאוניטרי בודד שניתן לחלק באופן לוגי ל- כפי שמוצג באיור למטה.

הפצת אופרטורים אחורה סופגת את האוניטרי לתוך האובזרבבל על ידי התפתחותו כ-. במילים אחרות, חלק מהחישוב מבוצע קלאסית באמצעות ההתפתחות של האובזרבבל מ- ל-. ניתן כעת לנסח מחדש את הבעיה המקורית כמדידת האובזרבבל עבור המעגל החדש בעומק נמוך יותר שהאוניטרי שלו הוא .
האוניטרי מיוצג כמספר פרוסות . קיימות דרכים מרובות להגדרת פרוסה. לדוגמה, במעגל הדוגמה לעיל, כל שכבה של וכל שכבה של שערי יכולה להיחשב כפרוסה בודדת. הפצה אחורה כוללת חישוב של באופן קלאסי. כל פרוסה יכולה להיות מיוצגת כ-, כאשר הוא Pauli של -קיוביטים ו- הוא סקלר. קל לוודא ש:
בדוגמה לעיל, אם , אז עלינו להריץ שני מעגלים קוונטיים, במקום אחד, כדי לחשב את ערך התוחלת. לכן, הפצה אחורה עשויה להגדיל את מספר האיברים באובזרבבל, מה שמוביל למספר גבוה יותר של ביצועי מעגלים. דרך אחת לאפשר הפצה אחורה עמוקה יותר לתוך המעגל, תוך מניעת האופרטור מלגדול יותר מדי, היא לקטום איברים עם מקדמים קטנים, במקום להוסיף אותם לאופרטור. לדוגמה, בדוגמה לעיל, אפשר לבחור לקטום את האיבר הכולל בתנאי ש- קטן מספיק. קיטום איברים יכול להביא למעגלים קוונטיים פחותים לביצוע, אך פעולה זו גורמת לשגיאה מסוימת בחישוב ערך התוחלת הסופי פרופורציונלית לגודל המקדמים של האיברים הקטומים.
מדריך זה מממש תבנית Qiskit לסימולציית הדינמיקה הקוונטית של שרשרת ספין היזנברג באמצעות qiskit-addon-obp.
דרישות
לפני תחילת מדריך זה, וודא שהדברים הבאים מותקנים:
- Qiskit SDK v1.2 ואילך (
pip install qiskit) - Qiskit Runtime v0.28 ואילך (
pip install qiskit-ibm-runtime) - תוסף OBP Qiskit (
pip install qiskit-addon-obp) - כלי עזר לתוספי Qiskit (
pip install qiskit-addon-utils)
הגדרה
import numpy as np
import matplotlib.pyplot as plt
from qiskit.primitives import StatevectorEstimator as Estimator
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import CouplingMap
from qiskit.synthesis import LieTrotter
from qiskit_addon_utils.problem_generators import generate_xyz_hamiltonian
from qiskit_addon_utils.problem_generators import (
generate_time_evolution_circuit,
)
from qiskit_addon_utils.slicing import slice_by_gate_types, combine_slices
from qiskit_addon_obp.utils.simplify import OperatorBudget
from qiskit_addon_obp import backpropagate
from qiskit_addon_obp.utils.truncating import setup_budget
from rustworkx.visualization import graphviz_draw
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import EstimatorV2, EstimatorOptions
חלק I: שרשרת ספין היזנברג בקנה מידה קטן
שלב 1: מיפוי קלטים קלאסיים לבעיה קוונטית
מיפוי ההתפתחות בזמן של מודל היזנברג קוונטי לניסוי קוונטי.
חבילת qiskit_addon_utils מספקת פונקציונליות לשימוש חוזר למטרות שונות.
מודול qiskit_addon_utils.problem_generators שלה מספק פונקציות ליצירת המילטוניאנים דמויי היזנברג על גרף קישוריות נתון. גרף זה יכול להיות או rustworkx.PyGraph או CouplingMap מה שהופך אותו לקל לשימוש בזרימות עבודה ממוקדות Qiskit.
להלן, אנו יוצרים CouplingMap שרשרת ליניארית של 10 קיוביטים.
num_qubits = 10
layout = [(i - 1, i) for i in range(1, num_qubits)]
# Instantiate a CouplingMap object
coupling_map = CouplingMap(layout)
graphviz_draw(coupling_map.graph, method="circo")
לאחר מכן, אנו יוצרים אופרטור Pauli המדגמן המילטוניאן XYZ היזנברג.
כאשר הוא הגרף של מפת הקישוריות שסופקה.
# Get a qubit operator describing the Heisenberg XYZ model
hamiltonian = generate_xyz_hamiltonian(
coupling_map,
coupling_constants=(np.pi / 8, np.pi / 4, np.pi / 2),
ext_magnetic_field=(np.pi / 3, np.pi / 6, np.pi / 9),
)
print(hamiltonian)
SparsePauliOp(['IIIIIIIXXI', 'IIIIIIIYYI', 'IIIIIIIZZI', 'IIIIIXXIII', 'IIIIIYYIII', 'IIIIIZZIII', 'IIIXXIIIII', 'IIIYYIIIII', 'IIIZZIIIII', 'IXXIIIIIII', 'IYYIIIIIII', 'IZZIIIIIII', 'IIIIIIIIXX', 'IIIIIIIIYY', 'IIIIIIIIZZ', 'IIIIIIXXII', 'IIIIIIYYII', 'IIIIIIZZII', 'IIIIXXIIII', 'IIIIYYIIII', 'IIIIZZIIII', 'IIXXIIIIII', 'IIYYIIIIII', 'IIZZIIIIII', 'XXIIIIIIII', 'YYIIIIIIII', 'ZZIIIIIIII', 'IIIIIIIIIX', 'IIIIIIIIIY', 'IIIIIIIIIZ', 'IIIIIIIIXI', 'IIIIIIIIYI', 'IIIIIIIIZI', 'IIIIIIIXII', 'IIIIIIIYII', 'IIIIIIIZII', 'IIIIIIXIII', 'IIIIIIYIII', 'IIIIIIZIII', 'IIIIIXIIII', 'IIIIIYIIII', 'IIIIIZIIII', 'IIIIXIIIII', 'IIIIYIIIII', 'IIIIZIIIII', 'IIIXIIIIII', 'IIIYIIIIII', 'IIIZIIIIII', 'IIXIIIIIII', 'IIYIIIIIII', 'IIZIIIIIII', 'IXIIIIIIII', 'IYIIIIIIII', 'IZIIIIIIII', 'XIIIIIIIII', 'YIIIIIIIII', 'ZIIIIIIIII'],
coeffs=[0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,
0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,
1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,
0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,
0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,
1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,
0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 1.04719755+0.j,
0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,
1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,
0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,
1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,
0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
0.34906585+0.j])
מאופרטור הקיוביטים, אנו יכולים ליצור מעגל קוונטי המדגמן את התפתחותו בזמן. שוב, מודול qiskit_addon_utils.problem_generators מגיע להצלה עם פונקציה שימושית לעשות בדיוק זאת:
circuit = generate_time_evolution_circuit(
hamiltonian,
time=0.2,
synthesis=LieTrotter(reps=2),
)
circuit.draw("mpl", style="iqp", scale=0.6)
שלב 2: אופטימיזציה של הבעיה לביצוע על חומרה קוונטית
יצירת פרוסות מעגל להפצה אחורה
זכור, הפונקציה backpropagate תפיץ אחורה פרוסות מעגל שלמות בכל פעם, כך שהבחירה כיצד לפרוס יכולה להשפיע על מידת הביצועים של ההפצה האחורה עבור בעיה נתונה. כאן, נקבץ שערים מאותו סוג לפרוסות באמצעות פונקציית slice_by_gate_types.
לדיון מפורט יותר על חיתוך מעגלים, בדוק את מדריך הוראות זה של חבילת qiskit-addon-utils.
slices = slice_by_gate_types(circuit)
print(f"Separated the circuit into {len(slices)} slices.")
Separated the circuit into 18 slices.
הגבלת עד כמה האופרטור עשוי לגדול במהלך הפצה אחורה
במהלך הפצה אחורה, מספר האיברים באופרטור יתקרב בדרך כלל ל- במהירות, כאשר הוא מספר הקיוביטים. כאשר שני איברים באופרטור אינם קומוטטיביים מבחינת קיוביטים, אנו זקוקים למעגלים נפרדים כדי לקבל את ערכי התוחלת המתאימים להם. לדוגמה, אם יש לנו אובזרבבל של 2 קיוביטים , אז מכיוון ש-, מדידה בבסיס בודד מספיקה לחישוב ערכי התוחלת עבור שני איברים אלה. עם זאת, אנטי-קומוטטיבי עם שני האיברים האחרים. לכן אנו זקוקים למדידת בסיס נפרדת כדי לחשב את ערך התוחלת של . במילים אחרות, אנו זקוקים לשניים, במקום אחד, מעגל כדי לחשב . ככל שמספר האיברים באופרטור גדל, יש אפשרות שמספר ביצועי המעגלים הנדרשים גם יגדל.
ניתן להגביל את גודל האופרטור על ידי ציון הארגומנט operator_budget של הפונקציה backpropagate, המקבל מופע OperatorBudget.
כדי לשלוט בכמות המשאבים הנוספים (זמן) המוקצים, אנו מגבילים את המספר המרבי של קבוצות Pauli קומוטטיביות מבחינת קיוביטים שהאובזרבבל המופץ אחורה רשאי להחזיק. כאן אנו מציינים שההפצה האחורה צריכה להיפסק כאשר מספר קבוצות Pauli הקומוטטיביות מבחינת קיוביטים באופרטור גדל מעבר ל-8.
op_budget = OperatorBudget(max_qwc_groups=8)
הפצת פרוסות אחורה מהמעגל
ראשית נציין את האובזרבבל כ-, כאשר הוא מספר הקיוביטים. נפיץ אחורה פרוסות ממעגל התפתחות הזמן עד שהאיברים באובזרבבל לא יכולים עוד להיות משולבים לשמונה או פחות קבוצות Pauli קומוטטיביות מבחינת קיוביטים.
observable = SparsePauliOp.from_sparse_list(
[("Z", [i], 1 / num_qubits) for i in range(num_qubits)],
num_qubits=num_qubits,
)
observable
SparsePauliOp(['IIIIIIIIIZ', 'IIIIIIIIZI', 'IIIIIIIZII', 'IIIIIIZIII', 'IIIIIZIIII', 'IIIIZIIIII', 'IIIZIIIIII', 'IIZIIIIIII', 'IZIIIIIIII', 'ZIIIIIIIII'],
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])
להלן תראה שהפצנו אחורה שש פרוסות, והאיברים שולבו לשש ולא שמונה קבוצות. זה מרמז שהפצת פרוסה נוספת אחורה תגרום למספר קבוצות Pauli לחרוג משמונה. אנו יכולים לאמת שזה המקרה על ידי בדיקת המטא-דאטה המוחזרת. שים לב גם שבחלק זה טרנספורמציית המעגל מדויקת. כלומר, אף אחד מהאיברים של האובזרבבל החדש לא נקטם. המעגל המופץ אחורה והאופרטור המופץ אחורה נותנים את התוצאה המדויקת כמו המעגל והאופרטור המקוריים.
# Backpropagate slices onto the observable
bp_obs, remaining_slices, metadata = backpropagate(
observable, slices, operator_budget=op_budget
)
# Recombine the slices remaining after backpropagation
bp_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {metadata.num_backpropagated_slices} slices.")
print(
f"New observable has {len(bp_obs.paulis)} terms, which can be combined into {len(bp_obs.group_commuting(qubit_wise=True))} groups."
)
print(
f"Note that backpropagating one more slice would result in {metadata.backpropagation_history[-1].num_paulis[0]} terms "
f"across {metadata.backpropagation_history[-1].num_qwc_groups} groups."
)
print("The remaining circuit after backpropagation looks as follows:")
bp_circuit.draw("mpl", fold=-1, scale=0.6)
Backpropagated 6 slices.
New observable has 60 terms, which can be combined into 6 groups.
Note that backpropagating one more slice would result in 114 terms across 12 groups.
The remaining circuit after backpropagation looks as follows:
לאחר מכן, נציין את אותה בעיה עם אותן אילוצים על גודל האובזרבבל הפלט. עם זאת, הפעם, נקצה תקציב שגיאה לכל פרוסה באמצעות הפונקציה setup_budget. איברי Pauli עם מקדמים קטנים ייקטמו מכל פרוסה עד שתקציב השגיאה ימולא, ותקציב שנותר יתווסף לתקציב הפרוסה הבאה. שים לב שבמקרה זה, הטרנספורמציה עקב הפצה אחורה היא משוערת מכיוון שחלק מהאיברים באופרטור נקטמים.
כדי לאפשר קיטום זה, עלינו להגדיר את תקציב השגיאה שלנו כך:
truncation_error_budget = setup_budget(max_error_per_slice=0.005)
שים לב שעל ידי הקצאת שגיאת 5e-3 לכל פרוסה לקיטום, אנו מסוגלים להסיר פרוסה נוספת אחת מהמעגל, תוך הישארות בתקציב המקורי של שמונה קבוצות Pauli קומוטטיביות באובזרבבל. כברירת מחדל, backpropagate משתמש בנורמת L1 של המקדמים הקטומים כדי להגביל את השגיאה הכוללת שנגרמה מקיטום. לאפשרויות אחרות עיין במדריך הוראות על ציון p_norm.
בדוגמה מסוימת זו שבה הפצנו אחורה שבע פרוסות, שגיאת הקיטום הכוללת לא צריכה לחרוג מ-(5e-3 error/slice) * (7 slices) = 3.5e-2.
לדיון נוסף על הפצת תקציב שגיאה על פני הפרוסות שלך, בדוק את מדריך הוראות זה.
# Run the same experiment but truncate observable terms with small coefficients
bp_obs_trunc, remaining_slices_trunc, metadata = backpropagate(
observable,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
# Recombine the slices remaining after backpropagation
bp_circuit_trunc = combine_slices(
remaining_slices_trunc, include_barriers=False
)
print(f"Backpropagated {metadata.num_backpropagated_slices} slices.")
print(
f"New observable has {len(bp_obs_trunc.paulis)} terms, which can be combined into {len(bp_obs_trunc.group_commuting(qubit_wise=True))} groups.\n"
f"After truncation, the error in our observable is bounded by {metadata.accumulated_error(0):.3e}"
)
print(
f"Note that backpropagating one more slice would result in {metadata.backpropagation_history[-1].num_paulis[0]} terms "
f"across {metadata.backpropagation_history[-1].num_qwc_groups} groups."
)
print("The remaining circuit after backpropagation looks as follows:")
bp_circuit_trunc.draw("mpl", scale=0.6)
Backpropagated 7 slices.
New observable has 82 terms, which can be combined into 8 groups.
After truncation, the error in our observable is bounded by 3.266e-02
Note that backpropagating one more slice would result in 114 terms across 12 groups.
The remaining circuit after backpropagation looks as follows:
אנו שמים לב שקיטום מאפשר לנו להפיץ אחורה עוד יותר ללא עלייה במספר הקבוצות הקומוטטיביות באובזרבבל.
עכשיו שיש לנו את ה-ansatz המופחת והאובזרבבלים המורחבים שלנו, אנו יכולים להעביר את הניסויים שלנו ל-backend.
כאן נשתמש במחשב קוונטי IBM® בן 127 קיוביטים כדי להדגים כיצד להעביר ל-backend QPU.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
# Transpile original experiment
circuit_isa = pm.run(circuit)
observable_isa = observable.apply_layout(circuit_isa.layout)
# Transpile backpropagated experiment
bp_circuit_isa = pm.run(bp_circuit)
bp_obs_isa = bp_obs.apply_layout(bp_circuit_isa.layout)
# Transpile the backpropagated experiment with truncated observable terms
bp_circuit_trunc_isa = pm.run(bp_circuit_trunc)
bp_obs_trunc_isa = bp_obs_trunc.apply_layout(bp_circuit_trunc_isa.layout)
אנו יוצרים את Primitive Unified Bloc (PUB) עבור כל אחד משלושת המקרים.
pub = (circuit_isa, observable_isa)
bp_pub = (bp_circuit_isa, bp_obs_isa)
bp_trunc_pub = (bp_circuit_trunc_isa, bp_obs_trunc_isa)
שלב 3: ביצוע באמצעות primitives של Qiskit
חישוב ערך תוחלת
לבסוף, אנו יכולים להריץ את הניסויים המופצים אחורה ולהשוות אותם עם הניסוי המלא באמצעות StatevectorEstimator ללא רעש.
ideal_estimator = Estimator()
# Run the experiments using Estimator primitive to obtain the exact outcome
result_exact = (
ideal_estimator.run([(circuit, observable)]).result()[0].data.evs.item()
)
print(f"Exact expectation value: {result_exact}")
Exact expectation value: 0.8871244838989416
נשתמש ב-resilience_level = 2 עבור דוגמה זו.
options = EstimatorOptions()
options.default_precision = 0.011
options.resilience_level = 2
estimator = EstimatorV2(mode=backend, options=options)
job = estimator.run([pub, bp_pub, bp_trunc_pub])