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

תחילת העבודה עם נוסחאות מרובות-מכפלה (MPF)

תחילת העבודה עם נוסחאות מרובות-מכפלה (MPF)

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-addon-utils~=0.3.0
qiskit-addon-mpf~=0.3.0
scipy~=1.16.3

מדריך זה מדגים כיצד להשתמש בחבילה qiskit-addon-mpf, תוך שימוש באבולוציה בזמן של מודל איזינג כדוגמה. באמצעות חבילה זו תוכל לבנות נוסחת מרובות-מכפלה (MPF) שמשיגה שגיאת טרוטר נמוכה יותר במדידות של ערכי ציפייה. הכלים המסופקים מאפשרים לך לקבוע את משקולות ה-MPF שנבחר, שאותן ניתן להשתמש בהן כדי לשלב מחדש את ערכי הציפייה המוערכים ממספר Circuit-ים של אבולוציה בזמן, כל אחד עם מספר שונה של צעדי טרוטר.

התחל בהתבוננות בהמילטוניאן של מודל איזינג עם 10 אתרים:

HIsing=i=19Ji,(i+1)ZiZi+1+i=110hiXiH_{\text{Ising}} = \sum_{i=1}^9 J_{i,(i+1)}Z_iZ_{i+1} + \sum_{i=1}^{10} h_i X_i

כאשר Ji,(i+1)J_{i,(i+1)} הוא עוצמת הצימוד ו-hih_i הוא עוצמת השדה המגנטי החיצוני. להגדרת הבעיה, האובסרבבל שיימדד יהיה המגנטיזציה הכוללת של המערכת

M=i=110Zi.\langle M \rangle = \sum_{i=1}^{10} \langle Z_i \rangle.

קטע הקוד שלהלן מכין את ההמילטוניאן של שרשרת האיזינג באמצעות חבילת qiskit-addon-utils, ומגדיר את האובסרבבל שיימדד.

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-mpf qiskit-addon-utils scipy
from qiskit.transpiler import CouplingMap
from qiskit.synthesis import SuzukiTrotter
from qiskit.quantum_info import SparsePauliOp
from qiskit.primitives import StatevectorEstimator
from qiskit.providers.fake_provider import GenericBackendV2
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_addon_utils.problem_generators import (
generate_xyz_hamiltonian,
generate_time_evolution_circuit,
)
from qiskit_addon_mpf.costs import (
setup_exact_problem,
setup_sum_of_squares_problem,
)
from qiskit_addon_mpf.static import setup_static_lse

from scipy.linalg import expm
import numpy as np

# Generate some coupling map to use for this example
coupling_map = CouplingMap.from_line(10, bidirectional=False)

# Get a qubit operator describing the Ising field model
hamiltonian = generate_xyz_hamiltonian(
coupling_map,
coupling_constants=(0.0, 0.0, 1.0),
ext_magnetic_field=(0.4, 0.0, 0.0),
)
print(f"Hamiltonian:\n {hamiltonian}\n")

L = coupling_map.size()
observable = SparsePauliOp.from_sparse_list(
[("Z", [i], 1 / L / 2) for i in range(L)], num_qubits=L
)
print(f"Observable:\n {observable}")
Hamiltonian:
SparsePauliOp(['IIIIIIIZZI', 'IIIIIZZIII', 'IIIZZIIIII', 'IZZIIIIIII', 'IIIIIIIIZZ', 'IIIIIIZZII', 'IIIIZZIIII', 'IIZZIIIIII', 'ZZIIIIIIII', 'IIIIIIIIIX', 'IIIIIIIIXI', 'IIIIIIIXII', 'IIIIIIXIII', 'IIIIIXIIII', 'IIIIXIIIII', 'IIIXIIIIII', 'IIXIIIIIII', 'IXIIIIIIII', 'XIIIIIIIII'],
coeffs=[1. +0.j, 1. +0.j, 1. +0.j, 1. +0.j, 1. +0.j, 1. +0.j, 1. +0.j, 1. +0.j,
1. +0.j, 0.4+0.j, 0.4+0.j, 0.4+0.j, 0.4+0.j, 0.4+0.j, 0.4+0.j, 0.4+0.j,
0.4+0.j, 0.4+0.j, 0.4+0.j])

Observable:
SparsePauliOp(['IIIIIIIIIZ', 'IIIIIIIIZI', 'IIIIIIIZII', 'IIIIIIZIII', 'IIIIIZIIII', 'IIIIZIIIII', 'IIIZIIIIII', 'IIZIIIIIII', 'IZIIIIIIII', 'ZIIIIIIIII'],
coeffs=[0.05+0.j, 0.05+0.j, 0.05+0.j, 0.05+0.j, 0.05+0.j, 0.05+0.j, 0.05+0.j,
0.05+0.j, 0.05+0.j, 0.05+0.j])

כעת תכין את ה-MPF. הבחירה הראשונה היא האם המקדמים יהיו סטטיים (בלתי תלויים בזמן) או דינמיים; מדריך זה משתמש ב-MPF סטטי. הבחירה הבאה היא קבוצת ערכי kjk_j. זה קובע כמה איברים יהיו ב-MPF, וכן כמה צעדי טרוטר כל איבר משתמש בהם לסימולציה של האבולוציה בזמן. בחירת ערכי kjk_j היא תהליך היוריסטי, ולכן עליך להשיג סט משלך של ערכי kjk_j "טובים". קרא הנחיות לאיתור סט ערכים טוב בסוף דף תחילת העבודה.

לאחר מכן, ברגע שנקבעו ערכי kjk_j, ניתן להכין את מערכת המשוואות Ax=bAx=b לפתרון. המטריצה AA נקבעת גם היא על ידי נוסחת המכפלה שתשמש. הבחירות כאן הן סדרה, שמוגדרת כ-22 בדוגמה זו, וכן האם נוסחת המכפלה צריכה להיות סימטרית, שמוגדרת כ-True בדוגמה זו. קטע הקוד שלהלן בוחר זמן כולל לאבולוציה של המערכת, ערכי kjk_j לשימוש, ואת מערכת המשוואות לפתרון באמצעות המתודה qiskit_addon_mpf.static.setup_static_lse.

time = 8.0
trotter_steps = (8, 12, 19)

lse = setup_static_lse(trotter_steps, order=2, symmetric=True)
print(lse)
LSE(A=array([[1.00000000e+00, 1.00000000e+00, 1.00000000e+00],
[1.56250000e-02, 6.94444444e-03, 2.77008310e-03],
[2.44140625e-04, 4.82253086e-05, 7.67336039e-06]]), b=array([1., 0., 0.]))

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

model_exact, coeffs_exact = setup_exact_problem(lse)
model_approx, coeffs_approx = setup_sum_of_squares_problem(
lse, max_l1_norm=3.0
)
model_exact.solve()
model_approx.solve()
print(f"Exact solution: {coeffs_exact.value}")
print(f"Approximate solution: {coeffs_approx.value}")
Exact solution: [ 0.17239057 -1.19447005  2.02207947]
Approximate solution: [-0.40454257 0.57553173 0.8290123 ]
הערה

לאובייקט LSE יש גם מתודה LSE.solve() שפותרת את מערכת המשוואות באופן מדויק. הסיבה שנעשה שימוש ב-setup_exact_problem() במדריך זה היא להדגים את הממשק שמספקות השיטות הקירוביות האחרות.

הגדרה וביצוע של Circuit-ים של טרוטר

כעת שהמקדמים xjx_j התקבלו, השלב האחרון הוא ליצור את Circuit-ים של אבולוציה בזמן עבור הסדר וקבוצת הצעדים kjk_j שנבחרו של ה-MPF. חבילת qiskit-addon-utils יכולה לזרז תהליך זה.

circuits = []
for k in trotter_steps:
circ = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(order=2, reps=k),
time=time,
)
circuits.append(circ)
circuits[0].draw("mpl", fold=-1)

Output of the previous code cell

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

Output of the previous code cell

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

Output of the previous code cell

לאחר שה-Circuit-ים הללו נבנו, תוכל לבצע להם Transpile ולהריץ אותם על QPU. בדוגמה זו נשתמש באחד מהסימולטורים ללא רעש כדי להדגים כיצד שגיאת טרוטר מצטמצמת.

backend = GenericBackendV2(num_qubits=10)
transpiler = generate_preset_pass_manager(
optimization_level=2, backend=backend
)

transpiled_circuits = [transpiler.run(circ) for circ in circuits]

estimator = StatevectorEstimator()
job = estimator.run([(circ, observable) for circ in transpiled_circuits])
result = job.result()

mpf_evs = [res.data.evs for res in result]
print(mpf_evs)
[array(0.23799162), array(0.35754312), array(0.38649906)]

שחזור תוצאות

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

M=jxjMj.\langle M \rangle = \sum_j x_j \langle M_j \rangle.

כאשר xjx_j הם המקדמים שמצאנו ו-Mj\langle M_j \rangle הוא האומדן של האובסרבבל iZi\sum_i \langle Z_i \rangle עבור כל Circuit. לאחר מכן נוכל להשוות את התוצאות שהתקבלו עם הערך המדויק באמצעות חבילת scipy.linalg.

exp_H = expm(-1j * time * hamiltonian.to_matrix())
initial_state = np.zeros(exp_H.shape[0])
initial_state[0] = 1.0

time_evolved_state = exp_H @ initial_state
exact_obs = (
time_evolved_state.conj() @ observable.to_matrix() @ time_evolved_state
)

# Print out the different observable measurements
print(f"Exact value: {exact_obs.real}")
print(f"PF with 19 steps: {mpf_evs[-1]}")
print(f"MPF using exact solution: {mpf_evs @ coeffs_exact.value}")
print(f"MPF using approximate solution: {mpf_evs @ coeffs_approx.value}")
Exact value: 0.4006024248789992
PF with 19 steps: 0.3864990619977402
MPF using exact solution: 0.3954847855979902
MPF using approximate solution: 0.4299121425348959

כאן ניתן לראות שה-MPF הצליח להפחית את שגיאת טרוטר בהשוואה לזו שהתקבלה עם PF בודד עם kj=19k_j=19. עם זאת, המודל הקירובי הניב ערך ציפייה גרוע יותר מהמודל המדויק. הדבר ממחיש את החשיבות של שימוש בקריטריוני התכנסות מחמירים על המודל הקירובי ואיתור קבוצת kjk_j "טובה".