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

תחילת העבודה עם OBP

Package versions

The code on this page was developed using the following requirements. We recommend using these versions or newer.

qiskit[all]~=2.3.0
qiskit-ibm-runtime~=0.43.1
qiskit-addon-utils~=0.3.0
qiskit-addon-obp~=0.3.0

כאשר אתה מכין עומס עבודה קוונטי עם הפצה לאחור של אופרטורים (OBP), תחילה עליך לבצע בחירה של "פרוסות Circuit", ושנית, עליך לציין סף חיתוך או "תקציב שגיאה" להסרת איברים עם מקדמים קטנים באופרטור המופץ לאחור וכן להגדיר חסם עליון על הגודל הכולל של האופרטור המופץ לאחור. במהלך ההפצה לאחור, מספר האיברים באופרטור של Circuit עם NN Qubit-ים יתקרב ל-4N4^N במהרה בתרחיש המקרה הגרוע ביותר. מדריך זה מדגים את הצעדים הכרוכים ביישום OBP על עומס עבודה קוונטי.

המרכיב העיקרי של חבילת qiskit-addons-obp הוא הפונקציה backpropagate(). היא מקבלת ארגומנטים עבור האובסרבבל הסופי לשחזור, קבוצת פרוסות Circuit לחישוב קלאסי, ובאופן אופציונלי, TruncationErrorBudget או OperatorBudget לספק אילוצים על החיתוך שמבוצע. ברגע שאלה מוגדרים, האופרטור OO' המחושב קלאסית ומופץ לאחור מחושב בצורה איטרטיבית על ידי החלת ה-Gate-ים מכל פרוסה ss בדרך הבאה:

O(s)=USs+1O(s1)USs+1O'^{(s)} = \mathcal{U}_{S-s+1}^\dagger O'^{(s-1)} \mathcal{U}_{S-s+1}

כאשר SS הוא מספר הפרוסות הכולל ו-Us\mathcal{U}_{s} מייצג פרוסה בודדת של ה-Circuit. דוגמה זו משתמשת בחבילת qiskit-addons-utils להכנת פרוסות ה-Circuit וכן ליצירת ה-Circuit לדוגמה.

להתחלה, שקול את האבולוציה בזמן של שרשרת Heisenberg XYZ. להמילטוניאן זה יש את הצורה

H^=(j,k)(JxXjXk+JyYjYk+JzZjZk)+j(hxXj+hyYj+hzZj) \hat{H} = \sum_{(j,k)} \left( J_xX_jX_k + J_yY_jY_k + J_z Z_jZ_k \right) + \sum_{j} \left(h_xX_j + h_yY_j + h_zZ_j\right)

וערך הציפייה שיימדד יהיה Z0\langle Z_0 \rangle.

קטע הקוד הבא מייצר את ההמילטוניאן בצורת SparsePauliOp באמצעות המודול qiskit_addons_utils.problem_generators ו-CouplingMap. הגדר את קבועי הצימוד ל-Jx=π/8J_x=\pi/8, Jy=π/4J_y=\pi/4, Jz=π/2J_z=\pi/2 ושדות מגנטיים חיצוניים ל-hx=π/3h_x=\pi/3, hy=π/6h_y=\pi/6, hz=π/9h_z=\pi/9, ולאחר מכן ייצר Circuit המדמה את האבולוציה בזמן שלו.

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-obp qiskit-addon-utils qiskit-ibm-runtime
import numpy as np
from qiskit.transpiler import CouplingMap
from qiskit.synthesis import LieTrotter
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2
from qiskit_ibm_runtime.fake_provider import FakeMelbourneV2
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp
from qiskit_addon_utils.problem_generators import (
generate_time_evolution_circuit,
generate_xyz_hamiltonian,
)
from qiskit_addon_utils.slicing import slice_by_gate_types
from qiskit_addon_obp.utils.simplify import OperatorBudget
from qiskit_addon_obp.utils.truncating import setup_budget
from qiskit_addon_obp import backpropagate
from qiskit_addon_utils.slicing import combine_slices

coupling_map = CouplingMap.from_heavy_hex(3, bidirectional=False)

# Choose a 10-qubit linear chain on this coupling map
reduced_coupling_map = coupling_map.reduce(
[0, 13, 1, 14, 10, 16, 5, 12, 8, 18]
)

# Get a qubit operator describing the Heisenberg XYZ model
hamiltonian = generate_xyz_hamiltonian(
reduced_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),
)

# we evolve for some time
circuit = generate_time_evolution_circuit(
hamiltonian, synthesis=LieTrotter(reps=2), time=0.2
)

circuit.draw("mpl")

Output of the previous code cell

הכנת קלטים להפצה לאחור

כעת, ייצר את פרוסות ה-Circuit להפצה לאחור. באופן כללי, אופן הפירוס לפרוסות יכול להשפיע על ביצועי ההפצה לאחור עבור בעיה נתונה. כאן, קבץ Gate-ים מאותו סוג לפרוסות באמצעות הפונקציה qiskit_addons_utils.slice_by_gate_types.

slices = slice_by_gate_types(circuit)
print(f"Separated the circuit into {len(slices)} slices.")
Separated the circuit into 18 slices.

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

כאן, ציין שההפצה לאחור צריכה להפסיק כאשר מספר קבוצות הפאולי המתחלפות ב-Qubit-ים באופרטור יעבור 88, והקצה תקציב שגיאה של 0.0050.005 עבור כל פרוסה.

op_budget = OperatorBudget(max_qwc_groups=8)
truncation_error_budget = setup_budget(max_error_per_slice=0.005)

הפצה לאחור של פרוסות

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

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

Note

כברירת מחדל, backpropagate() משתמשת ב-L1L_1 נורמה של המקדמים שחותכו כדי לחסום את השגיאה הכוללת שנגרמת מהחיתוך, אך ניתן להשתמש ב-LpL_p אחרים אם ברצונך לשנות כיצד שגיאת החיתוך מחושבת.

# Specify a single-qubit observable
observable = SparsePauliOp("IIIIIIIIIZ")

# Backpropagate without the truncation error budget
backpropagated_observable, remaining_slices, metadata = backpropagate(
observable,
slices,
operator_budget=op_budget,
)

# Recombine the slices remaining after backpropagation
bp_circuit = combine_slices(remaining_slices, include_barriers=True)

print(f"Backpropagated {metadata.num_backpropagated_slices} slices.")
print(
f"New observable has {len(backpropagated_observable.paulis)} terms, which can be combined into "
f"{len(backpropagated_observable.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."
)
Backpropagated 7 slices.
New observable has 18 terms, which can be combined into 8 groups.
After truncation, the error in our observable is bounded by 0.000e+00
Note that backpropagating one more slice would result in 27 terms across 12 groups.
print(
"The remaining circuit after backpropagation without truncation looks as follows:"
)
bp_circuit.draw("mpl", scale=0.6)
The remaining circuit after backpropagation without truncation looks as follows:

Output of the previous code cell

קטעי הקוד שלהלן מבצעים הפצה לאחור של ה-Circuit עם תקציב שגיאת חיתוך.

# Backpropagate *with* the truncation error budget
backpropagated_observable_trunc, remaining_slices_trunc, metadata_trunc = (
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=True
)

print(f"Backpropagated {metadata_trunc.num_backpropagated_slices} slices.")
print(
f"New observable has {len(backpropagated_observable_trunc.paulis)} terms, which can be combined into "
f"{len(backpropagated_observable_trunc.group_commuting(qubit_wise=True))} groups.\n"
f"After truncation, the error in our observable is bounded by {metadata_trunc.accumulated_error(0):.3e}"
)
print(
f"Note that backpropagating one more slice would result in {metadata_trunc.backpropagation_history[-1].num_paulis[0]} terms "
f"across {metadata_trunc.backpropagation_history[-1].num_qwc_groups} groups."
)
Backpropagated 10 slices.
New observable has 19 terms, which can be combined into 8 groups.
After truncation, the error in our observable is bounded by 4.933e-02
Note that backpropagating one more slice would result in 27 terms across 13 groups.
print(
"The remaining circuit after backpropagation with truncation looks as follows:"
)
bp_circuit_trunc.draw("mpl", scale=0.6)
The remaining circuit after backpropagation with truncation looks as follows:

Output of the previous code cell

Transpile וביצוע עומס העבודה הקוונטי

כעת שביצעת הפצה לאחור של האופרטור, תוכל להריץ את החלק הנותר של ה-Circuit על QPU. עומס העבודה הקוונטי, באמצעות ה-Estimator, צריך לכלול את ה-Circuit bp_circuit_trunc וחייב למדוד את האופרטור המופץ לאחור backpropagated_observable.

כדי להדגים את יעילות OBP בפני עצמה, קטע הקוד הבא מבצע Transpile לשני ה-Circuit-ים המקורי והמופץ לאחור (עם וללא חיתוך) ומדמה את ה-Circuit-ים קלאסית באמצעות StatevectorEstimator.

# Specify a backend and a pass manager for transpilation
backend = FakeMelbourneV2()
# pm = generate_preset_pass_manager(backend=backend, optimization_level=1)

pm = generate_preset_pass_manager(backend=backend, optimization_level=3)

# Transpile original experiment
circuit_isa = pm.run(circuit)
observable_isa = observable.apply_layout(circuit_isa.layout)

# Transpile backpropagated experiment without truncation
bp_circuit_isa = pm.run(bp_circuit)
bp_obs_isa = backpropagated_observable.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 = backpropagated_observable_trunc.apply_layout(
bp_circuit_trunc_isa.layout
)

estimator = StatevectorEstimator()

# Run the experiments using the exact statevector estimator
result_exact = (
estimator.run([(circuit, observable)]).result()[0].data.evs.item()
)

result_bp = (
estimator.run([(bp_circuit_isa, bp_obs_isa)]).result()[0].data.evs.item()
)
result_bp_trunc = (
estimator.run([(bp_circuit_trunc_isa, bp_obs_trunc_isa)])
.result()[0]
.data.evs.item()
)

print(f"Exact expectation value: {result_exact}")
print(f"Backpropagated expectation value without truncation: {result_bp}")
print(f"Backpropagated expectation value with truncation: {result_bp_trunc}")
print(
f" - Expected Error for truncated observable: {metadata_trunc.accumulated_error(0):.3e}"
)
print(
f" - Observed Error for truncated observable: {abs(result_exact - result_bp_trunc):.3e}"
)
Exact expectation value: 0.8854160687717517
Backpropagated expectation value without truncation: 0.8854160687717533
Backpropagated expectation value with truncation: 0.8850236647156081
- Expected Error for truncated observable: 4.933e-02
- Observed Error for truncated observable: 3.924e-04

לבסוף, קטע הקוד הבא יבצע Transpile וישרוץ את ה-Circuit המופץ לאחור על QPU (גם עם וגם בלי חיתוך).

# Specify a backend and a pass manager for transpilation
service = QiskitRuntimeService()
backend = service.least_busy()
pm = generate_preset_pass_manager(backend=backend, optimization_level=3)

# Transpile backpropagated experiment without truncation
bp_circuit_isa = pm.run(bp_circuit)
bp_obs_isa = backpropagated_observable.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 = backpropagated_observable_trunc.apply_layout(
bp_circuit_trunc_isa.layout
)

# Run the experiments using Estimator primitive
estimator = EstimatorV2(mode=backend)

result_bp_qpu = (
estimator.run([(bp_circuit_isa, bp_obs_isa)]).result()[0].data.evs.item()
)

result_bp_trunc_qpu = (
estimator.run([(bp_circuit_trunc_isa, bp_obs_trunc_isa)])
.result()[0]
.data.evs.item()
)

print(f"Exact expectation value: {result_exact}")
print(f"Backpropagated expectation value without truncation: {result_bp_qpu}")
print(
f"Backpropagated expectation value with truncation: {result_bp_trunc_qpu}"
)
print(
f" - Observed Error for observable without truncation: {abs(result_exact - result_bp_qpu):.3e}"
)
print(
f" - Observed Error for truncated observable: {abs(result_exact - result_bp_trunc_qpu):.3e}"
)
Exact expectation value: 0.8854160687717517
Backpropagated expectation value without truncation: 0.8790435084647706
Backpropagated expectation value with truncation: 0.8759838342768448
- Observed Error for observable without truncation: 6.373e-03
- Observed Error for truncated observable: 9.432e-03

השלבים הבאים

Recommendations