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

צמצום עומק מעגלים עם הפצה לאחור של אופרטורים

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

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

  • שלב 1: מיפוי לבעיה קוונטית
    • מיפוי ההמילטוניאן המתפתח עם הזמן למעגל קוונטי
  • שלב 2: אופטימיזציה של הבעיה
    • פילוח המעגל
    • הפצה לאחור של פרוסות מהמעגל אל אובייקט Observable של פאולי
    • שילוב הפרוסות הנותרות למעגל בודד
    • Transpile של המעגל עבור ה-Backend
  • שלב 3: ביצוע ניסויים
    • חישוב ערך הציפייה באמצעות המעגל המצומצם וה-observable המורחב עם StatevectorEstimator לצורך פשטות במחברת זו
  • שלב 4: שחזור תוצאות
    • לא רלוונטי.

הערה: Qiskit מתאר באופן כללי שכבות כחלוקות עומק-1 של המעגל על פני כל ה-Qubits. חבילה זו עושה שימוש במונח פרוסות לתיאור שכבות בעומק שרירותי. הפונקציה qiskit_addon_obp.backpropagate מיועדת להפיץ לאחור פרוסות שלמות בכל פעם, ולכן הבחירה כיצד לפרוס את המעגל הקוונטי יכולה להשפיע רבות על ביצועי ההפצה לאחור עבור בעיה נתונה. תלמד עוד על פרוסות בהמשך.

שלב 1: מיפוי לבעיה קוונטית

מיפוי אבולוציית הזמן של מודל הייזנברג קוונטי לניסוי קוונטי.

חבילת qiskit_addon_utils מספקת פונקציונליות שניתנת לשימוש חוזר למטרות שונות.

המודול qiskit_addon_utils.problem_generators שלה מספק פונקציות ליצירת המילטוניאנים מסוג הייזנברג על גרף קישוריות נתון. גרף זה יכול להיות rustworkx.PyGraph או CouplingMap, מה שמקל על השימוש בו בתהליכי עבודה ממוקדי Qiskit.

בהמשך, נייצר תחילה CouplingMap מסוג heavy-hex ממנו נחצוב שרשרת לינארית של 10 Qubits. שים לב שהאינדקסים של reduced_coupling_map החדש מתחילים שוב מאפס.

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-obp qiskit-addon-utils qiskit-ibm-runtime rustworkx
from qiskit.transpiler import CouplingMap

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])
from rustworkx.visualization import graphviz_draw

graphviz_draw(reduced_coupling_map.graph, method="circo")

Code output

לאחר מכן, ניצור אופרטור פאולי המדמה המילטוניאן הייזנברג XYZ.

J_{y} \sigma_j^{y} \sigma_{k}^{y} + J_{z} \sigma_j^{z} \sigma_{k}^{z}) + \sum_{j\in V} (h_{x} \sigma_j^{x} + h_{y} \sigma_j^{y} + h_{z} \sigma_j^{z})$$ כאשר $G(V,E)$ הוא הגרף של מפת הקישוריות שסופקה. ```python import numpy as np from qiskit_addon_utils.problem_generators import generate_xyz_hamiltonian # 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), ) print(hamiltonian) ``` ```text 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](https://qiskit.github.io/qiskit-addon-utils/stubs/qiskit_addon_utils.problem_generators.html) בא לעזרתנו עם פונקציה נוחה בדיוק לכך: ```python from qiskit.synthesis import LieTrotter from qiskit_addon_utils.problem_generators import generate_time_evolution_circuit circuit = generate_time_evolution_circuit( hamiltonian, time=0.2, synthesis=LieTrotter(reps=2), ) circuit.draw("mpl", style="iqp", scale=0.6) ``` ![Quantum circuit diagram](/img/qiskit-addons/obp/01-getting-started/output_2.png) ## שלב 2: אופטימיזציה של הבעיה \{#step-2-optimize-the-problem} ### יצירת פרוסות מעגל להפצה לאחור \{#create-circuit-slices-to-backpropagate} זכור שהפונקציה ``backpropagate`` מפיצה לאחור פרוסות שלמות בכל פעם, ולכן הבחירה כיצד לפרוס יכולה להשפיע על ביצועי ההפצה לאחור עבור בעיה נתונה. כאן, נקבץ Gate-ים מאותו סוג לפרוסות באמצעות הפונקציה [slice_by_gate_types](https://qiskit.github.io/qiskit-addon-utils/stubs/qiskit_addon_utils.slicing.slice_by_gate_types.html). לדיון מפורט יותר על פילוח מעגלים, ראה ב[מדריך כיצד לעשות](https://qiskit.github.io/qiskit-addon-utils/how_tos/create_circuit_slices.html) של חבילת [qiskit-addon-utils](https://qiskit.github.io/qiskit-addon-utils/index.html). ```python from qiskit_addon_utils.slicing import slice_by_gate_types slices = slice_by_gate_types(circuit) print(f"Separated the circuit into {len(slices)} slices.") ``` ```text Separated the circuit into 18 slices. ``` ### הגבלת גודל האופרטור במהלך ההפצה לאחור \{#constrain-how-large-the-operator-may-grow-during-backpropagation} במהלך ההפצה לאחור, מספר האיברים באופרטור יתקרב בדרך כלל במהירות ל-$4^N$, כאשר $N$ הוא מספר ה-Qubits. ניתן להגביל את גודל האופרטור על ידי ציון ה-kwarg ‏``operator_budget`` של הפונקציה ``backpropagate``, שמקבל מופע של [OperatorBudget](https://qiskit.github.io/qiskit-addon-obp/stubs/qiskit_addon_obp.utils.simplify.OperatorBudget.html). כאן אנחנו מציינים שההפצה לאחור תעצור כאשר מספר קבוצות פאולי המתחלפות ב-qubit-wise באופרטור יעלה על 8. ```python from qiskit_addon_obp.utils.simplify import OperatorBudget op_budget = OperatorBudget(max_qwc_groups=8) ``` ### הפצה לאחור של פרוסות מהמעגל \{#backpropagate-slices-from-the-circuit} ראשית, נציין את ה-observable פאולי-Z על Qubit 0, ונפיץ לאחור פרוסות ממעגל אבולוציית הזמן עד שהאיברים ב-observable לא יוכלו עוד להיכלל ב-8 קבוצות פאולי מתחלפות ב-qubit-wise או פחות. להלן תראה שהפצנו לאחור 7 פרוסות אך השתמשנו רק ב-6 מתוך 8 קבוצות הפאולי שהוקצו. משמע זאת שהפצה לאחור של פרוסה נוספת תגרום למספר קבוצות הפאולי לחרוג מ-8. נוכל לאמת שכך הדבר על ידי בדיקת ה-metadata שהוחזר. ```python from qiskit.quantum_info import SparsePauliOp from qiskit_addon_obp import backpropagate from qiskit_addon_utils.slicing import combine_slices # Specify a single-qubit observable observable = SparsePauliOp("IIIIIIIIIZ") # 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, include_barriers=True) 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", scale=0.6) ``` ```text Backpropagated 7 slices. New observable has 18 terms, which can be combined into 8 groups. Note that backpropagating one more slice would result in 27 terms across 12 groups. The remaining circuit after backpropagation looks as follows: ``` ![Quantum circuit diagram](/img/qiskit-addons/obp/01-getting-started/output_3.png) לאחר מכן, נציין את אותה הבעיה עם אותן מגבלות על גודל ה-observable הפלט. אולם הפעם, נקצה תקציב שגיאה לכל פרוסה באמצעות הפונקציה [setup_budet](https://qiskit.github.io/qiskit-addon-obp/stubs/qiskit_addon_obp.utils.truncating.setup_budget.html). איברי פאולי עם מקדמים קטנים יקוצצו מכל פרוסה עד שתקציב השגיאה ימולא, ותקציב שנותר יתווסף לתקציב הפרוסה הבאה. כדי לאפשר קיצוץ זה, עלינו להגדיר את תקציב השגיאה שלנו כך: ```python from qiskit_addon_obp.utils.truncating import setup_budget truncation_error_budget = setup_budget(max_error_per_slice=0.005) ``` שים לב שבהקצאת שגיאה של `5e-3` לכל פרוסה עבור קיצוץ, אנחנו מסוגלים להסיר עוד 3 פרוסות מהמעגל, תוך שמירה על התקציב המקורי של 8 קבוצות פאולי מתחלפות ב-observable. כברירת מחדל, `backpropagate` משתמש בנורמת L1 של המקדמים שנוקצצו כדי להגביל את השגיאה הכוללת שנצברת מהקיצוץ. לאפשרויות נוספות ראה ב[מדריך כיצד לעשות בנושא ציון p_norm](https://qiskit.github.io/qiskit-addon-obp/how_tos/bound_error_using_p_norm.html). בדוגמה הספציפית הזו שבה הפצנו לאחור 10 פרוסות, שגיאת הקיצוץ הכוללת לא אמורה לחרוג מ-``(5e-3 error/slice) * (10 slices) = 5e-2``. לדיון נוסף על חלוקת תקציב שגיאה על פני הפרוסות, ראה ב[מדריך כיצד לעשות](https://qiskit.github.io/qiskit-addon-obp/how_tos/truncate_operator_terms.html) זה. ```python # 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=True) 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) ``` ```text 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. The remaining circuit after backpropagation looks as follows: ``` ![Quantum circuit diagram](/img/qiskit-addons/obp/01-getting-started/output_4.png) ### כעת שיש לנו את ה-ansatze המצומצמים וה-observables המורחבים, נוכל לבצע Transpile לניסויים שלנו עבור ה-Backend. \{#now-that-we-have-our-reduced-ansatze-and-expanded-observables-we-can-transpile-our-experiments-to-the-backend} כאן נשתמש ב-[FakeMelbourneV2](https://quantum.cloud.ibm.com/docs/api/qiskit-ibm-runtime/fake-provider-fake-melbourne-v2) בעל 14 ה-Qubits מ-[qiskit-ibm-runtime](https://quantum.cloud.ibm.com/docs/api/qiskit-ibm-runtime) כדי להדגים כיצד לבצע Transpile ל-Backend מסוג QPU. ```python from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit_ibm_runtime.fake_provider import FakeMelbourneV2 # Specify a backend and a pass manager for transpilation backend = FakeMelbourneV2() 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) ``` ## שלב 3: ביצוע ניסויים קוונטיים \{#step-3-execute-quantum-experiments} ### חישוב ערך הציפייה \{#calculate-expectation-value} לבסוף, נוכל להריץ את הניסויים עם ההפצה לאחור ולהשוות אותם לניסוי המלא באמצעות ה-[StatevectorEstimator](https://quantum.cloud.ibm.com/docs/api/qiskit/qiskit.primitives.StatevectorEstimator) חסר הרעש. נוכל לראות שערך הציפייה עם ההפצה לאחור ללא קיצוץ שקול לערך המדויק בגבולות הדיוק הנומרי. ערך הציפייה על האופרטור עם איברים מקוצצים מכיל שגיאה בסדר גודל של ``1e-4``, שנמצאת בתוך הסבילות הצפויה. **הערה:** אנחנו משתמשים ב-Estimator‏ Primitive מבוסס statevector להמחשת השפעת הקיצוץ על הפלט. להרצה על ה-Backend שאליו בוצע Transpile לניסויים בשלב 2, יש לייבא את [EstimatorV2](https://quantum.cloud.ibm.com/docs/api/qiskit-ibm-runtime/estimator-v2) מ-``qiskit-ibm-runtime`` ולהעביר את מופע ה-Backend לבנאי. ```python from qiskit.primitives import StatevectorEstimator as Estimator estimator = Estimator() # Run the experiments using Estimator primitive result_exact = estimator.run([(circuit_isa, observable_isa)]).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: {result_bp}") print(f"Backpropagated expectation value with truncation: {result_bp_trunc}") print(f" - Expected Error for truncated observable: {metadata.accumulated_error(0):.3e}") print(f" - Observed Error for truncated observable: {abs(result_exact - result_bp_trunc):.3e}") ``` ```text Exact expectation value: 0.8854160687717507 Backpropagated expectation value: 0.8854160687717532 Backpropagated expectation value with truncation: 0.8850236647156059 - Expected Error for truncated observable: 4.933e-02 - Observed Error for truncated observable: 3.924e-04 ``` <TutorialFeedback />