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

הפחתת עומק Circuit עם תוסף AQC-Tensor של Qiskit

במחברת זו נעבור שלב אחרי שלב על תבנית Qiskit תוך שימוש בקומפילציה קוונטית משוערת עם רשתות טנסוריות (AQC-Tensor) כדי להגיע לעומק Circuit נמוך יותר מזה שהיה נדרש בדרך כלל לביצוע אבולוציית Trotter.

אלו השלבים שנבצע:

  • שלב 1: מיפוי לבעיה קוונטית
    • אתחול ההמילטוניאן והאובזרבל/ים של הבעיה שלנו
    • יצירת מצב רשת טנסורית יעד עבור החלק הראשוני של ה-Circuit
    • יצירת Circuit בעומק נמוך המקרב את החלק הנדחס
    • יצירת אנזץ כללי מתוך ה-Circuit הזה
    • אופטימיזציה של הפרמטרים כדי לקרב את האנזץ ככל האפשר ליעד
    • הוספת שלבי Trotter הבאים לאנזץ המאוחסן
  • שלב 2: אופטימיזציה לחומרה יעד
    • Transpile של ה-Circuit לחומרה
  • שלב 3: ביצוע ניסויים
    • שימוש ב-backend מדומה לפשטות
  • שלב 4: שחזור תוצאות
    • לא רלוונטי; במקום זאת, פשוט נפלוט את האובזרבל הנמדד

שלב 1: מיפוי ל-Circuit קוונטי ואופרטור

הגדרת המילטוניאן מודל ואובזרבל

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

H^Ising=i=110Ji,(i+1)ZiZ(i+1)+hiXi,\hat{\mathcal{H}}_{\text{Ising}} = \sum_{i=1}^{10} J_{i,(i+1)} Z_i Z_{(i+1)} + h_i X_i \, ,

כאשר תנאי הגבול המחזוריים קובעים שעבור i=10i=10 מתקבל i+1=111i+1=11\rightarrow1, JJ הוא עוצמת הצימוד בין שני אתרים ו-hh הוא השדה המגנטי החיצוני.

# Added by doQumentation — required packages for this notebook
!pip install -q qiskit qiskit-addon-aqc-tensor qiskit-addon-utils qiskit-ibm-runtime quimb scipy
from qiskit.transpiler import CouplingMap
from qiskit_addon_utils.problem_generators import generate_xyz_hamiltonian

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

# Choose a 10-qubit circle on this coupling map
reduced_coupling_map = coupling_map.reduce([0, 13, 1, 14, 10, 16, 4, 15, 3, 9])

# Get a qubit operator describing the Ising field model
hamiltonian = generate_xyz_hamiltonian(
reduced_coupling_map,
coupling_constants=(0.0, 0.0, 1.0),
ext_magnetic_field=(0.4, 0.0, 0.0),
)

האובזרבל שנמדוד הוא המגנטיזציה הכוללת.

from qiskit.quantum_info import SparsePauliOp

L = reduced_coupling_map.size()
observable = SparsePauliOp.from_sparse_list([("Z", [i], 1 / L / 2) for i in range(L)], num_qubits=L)

קביעת כמה מתוך אבולוציית הזמן לסמלץ קלאסית

המטרה הכוללת שלנו היא לסמלץ אבולוציית זמן של ההמילטוניאן לעיל. אנחנו עושים זאת באמצעות אבולוציית Trotter, שאנחנו מחלקים לשני חלקים:

  1. חלק ראשוני שניתן לסמלץ עם מצבי מכפלת מטריצות (MPS). נבצע "קומפילציה" של חלק זה באמצעות AQC כפי שמוצג ב-https://arxiv.org/abs/2301.08609.
  2. חלק נוסף של ה-Circuit שיורץ על חומרה. נתכנן להשתמש ב-AQC-Tensor כדי לדחוס את Circuit אבולוציית הזמן שלנו עד לזמן t=4t=4, ואז לבצע אבולוציה בשלבי Trotter רגילים עד t=5t=5.

יצירת Circuitים לפני ואחרי הפיצול

כעת, לאחר שבחרנו לפצל ב-t=4t=4, נייצר שני Circuitים:

  1. Circuit "יעד" עבור חלק ה-AQC של האבולוציה, מ-ti=0t_i=0 עד tf=4t_f=4. מאחר שזה מסומלץ על ידי סימולטור רשת טנסורית, מספר השכבות משפיע על זמן הריצה רק בגורם קבוע, כך שנוכל להשתמש במספר נדיב של שכבות כדי למזער את שגיאת Trotter.
from qiskit.synthesis import SuzukiTrotter
from qiskit_addon_utils.problem_generators import generate_time_evolution_circuit

aqc_evolution_time = 4.0
aqc_target_num_trotter_steps = 45

aqc_target_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_target_num_trotter_steps),
time=aqc_evolution_time,
)
  1. Circuit אבולוציה המשכי, שמבצע אבולוציה מ-ti=4t_i=4 עד tf=5t_f=5. מאחר שזה ירוץ על חומרה קוונטית, רצוי להשתמש במספר מינימלי של שכבות Trotter.
subsequent_evolution_time = 1.0
subsequent_num_trotter_steps = 5

subsequent_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=subsequent_num_trotter_steps),
time=subsequent_evolution_time,
)

לצורך השוואה מאוחרת, ניצור גם Circuit שלישי: כזה שמבצע אבולוציה עבור aqc_evolution_time אך עם אותו זמן אבולוציה לשלב Trotter כמו ב-Circuit ההמשכי. זהו ה-Circuit שהיינו עובדים איתו לולא השתמשנו במספר נדיב של שלבי Trotter עבור Circuit היעד. נתייחס אליו כ_Circuit ההשוואה_.

aqc_comparison_num_trotter_steps = int(
subsequent_num_trotter_steps / subsequent_evolution_time * aqc_evolution_time
)
aqc_comparison_num_trotter_steps
20
comparison_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_comparison_num_trotter_steps),
time=aqc_evolution_time,
)

יצירת אנזץ ופרמטרים ראשוניים מ-Circuit Trotter עם פחות שלבים

ראשית, נבנה Circuit "טוב" שיש לו את אותו זמן אבולוציה כמו Circuit היעד, אך עם פחות שלבי Trotter (וכך גם פחות שכבות).

לאחר מכן, נעביר את ה-Circuit ה"טוב" הזה לפונקציה generate_ansatz_from_circuit של AQC-Tensor. פונקציה זו מנתחת את הקישוריות של שני ה-Qubitים ב-Circuit ומחזירה שני דברים:

  1. Circuit אנזץ כללי ומפורמטר עם אותה קישוריות של שני Qubitים כמו ה-Circuit הקלט; ו-
  2. פרמטרים שכאשר מציבים אותם באנזץ, מניבים את ה-Circuit הקלט (הטוב).

בקרוב ניקח את הפרמטרים האלה ונשנה אותם בצורה איטרטיבית כדי לקרב את Circuit האנזץ ככל האפשר ל-MPS היעד.

from qiskit_addon_aqc_tensor import generate_ansatz_from_circuit

aqc_ansatz_num_trotter_steps = 5

aqc_good_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_ansatz_num_trotter_steps),
time=aqc_evolution_time,
)

aqc_ansatz, aqc_initial_parameters = generate_ansatz_from_circuit(
aqc_good_circuit, qubits_initially_zero=True
)
aqc_ansatz.draw("mpl", fold=-1)

Quantum circuit diagram

print(f"Comparison circuit: depth {comparison_circuit.depth()}")
print(f"Target circuit: depth {aqc_target_circuit.depth()}")
print(f"Ansatz circuit: depth {aqc_ansatz.depth()}, with {len(aqc_initial_parameters)} parameters")
Comparison circuit: depth 120
Target circuit: depth 270
Ansatz circuit: depth 23, with 515 parameters

בחירת הגדרות לסימולציית רשת טנסורית

כאן אנחנו משתמשים בסימולטור רשת טנסורית המבוסס על quimb. בדוגמה זו אנחנו משתמשים בסימולטור מצב מכפלת מטריצות (MPS) של quimb, ואנחנו משתמשים ב-JAX לדיפרנציאציה אוטומטית. ראו את תיעוד ה-API למידע נוסף על אופן השימוש בסימולטור quimb.

from functools import partial

import quimb.tensor

from qiskit_addon_aqc_tensor.simulation.quimb import QuimbSimulator

simulator_settings = QuimbSimulator(
partial(quimb.tensor.CircuitMPS, max_bond=100, cutoff=1e-8),
autodiff_backend="jax",
)

בניית ייצוג מצב מכפלת מטריצות של מצב יעד ה-AQC

בשלב הבא, נבנה ייצוג מכפלת מטריצות של המצב שיש לקרב באמצעות AQC.

from qiskit_addon_aqc_tensor.simulation import tensornetwork_from_circuit

aqc_target_mps = tensornetwork_from_circuit(aqc_target_circuit, simulator_settings)

שימו לב שמאחר שבחרנו מספר נדיב של שלבי Trotter עבור מצב היעד, בפועל יש לו פחות שגיאת Trotter מאשר Circuit ההשוואה. אנחנו יכולים לחשב את הנאמנות (ψ1ψ22| \langle \psi_1 | \psi_2 \rangle |^2) של המצב שמוכן על ידי Circuit ההשוואה לעומת מצב היעד:

from qiskit_addon_aqc_tensor.simulation import compute_overlap

comparison_mps = tensornetwork_from_circuit(comparison_circuit, simulator_settings)
comparison_fidelity = abs(compute_overlap(comparison_mps, aqc_target_mps)) ** 2
comparison_fidelity
0.9996761790297157

אופטימיזציה של פרמטרי האנזץ באמצעות חישובי MPS

כאן אנחנו ממזערים את פונקציית העלות הפשוטה ביותר האפשרית, MaximizeStateFidelity, באמצעות אופטימייזר L-BFGS מ-scipy.

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

from scipy.optimize import OptimizeResult, minimize

from qiskit_addon_aqc_tensor.objective import MaximizeStateFidelity

objective = MaximizeStateFidelity(aqc_target_mps, aqc_ansatz, simulator_settings)

stopping_point = 1 - comparison_fidelity

def callback(intermediate_result: OptimizeResult):
print(f"Intermediate result: Fidelity {1 - intermediate_result.fun:.8}")
if intermediate_result.fun < stopping_point:
# Good enough for now
raise StopIteration

result = minimize(
objective.loss_function,
aqc_initial_parameters,
method="L-BFGS-B",
jac=True,
options={"maxiter": 100},
callback=callback,
)
if result.status not in (
0,
1,
99,
): # 0 => success; 1 => max iterations reached; 99 => early termination via StopIteration
raise RuntimeError(f"Optimization failed: {result.message} (status={result.status})")

print(f"Done after {result.nit} iterations.")
aqc_final_parameters = result.x
Intermediate result: Fidelity 0.95080335
Intermediate result: Fidelity 0.98408927
Intermediate result: Fidelity 0.99140876
Intermediate result: Fidelity 0.9951876
Intermediate result: Fidelity 0.99563147
Intermediate result: Fidelity 0.99646297
Intermediate result: Fidelity 0.99679298
Intermediate result: Fidelity 0.99715793
Intermediate result: Fidelity 0.99756604
Intermediate result: Fidelity 0.99804283
Intermediate result: Fidelity 0.99832283
Intermediate result: Fidelity 0.99856583
Intermediate result: Fidelity 0.99868698
Intermediate result: Fidelity 0.998867
Intermediate result: Fidelity 0.99902237
Intermediate result: Fidelity 0.99912174
Intermediate result: Fidelity 0.99919705
Intermediate result: Fidelity 0.99926724
Intermediate result: Fidelity 0.99938605
Intermediate result: Fidelity 0.99951297
Intermediate result: Fidelity 0.99956172
Intermediate result: Fidelity 0.99962274
Intermediate result: Fidelity 0.99963919
Intermediate result: Fidelity 0.99967423
Intermediate result: Fidelity 0.9997101
Done after 25 iterations.

בניית ה-Circuit הסופי להעברה ל-Transpiler

final_circuit = aqc_ansatz.assign_parameters(aqc_final_parameters)
final_circuit.compose(subsequent_circuit, inplace=True)
final_circuit.draw("mpl", fold=-1)

Quantum circuit diagram

שלב 2: Transpile לביצוע על חומרה יעד

בשלב 2 של תבנית Qiskit, אנחנו מבצעים Transpile לה-Circuit ולכל האובזרבל/ים הרצויים לביצוע על מכשיר יעד. כאן אנחנו משתמשים ב-Backend מדומה שמסופק על ידי qiskit-ibm-runtime.

from qiskit import transpile
from qiskit_ibm_runtime.fake_provider import FakeMelbourneV2

backend = FakeMelbourneV2()

isa_circuit = transpile(final_circuit, backend)
isa_observable = observable.apply_layout(isa_circuit.layout)

לאחר מכן ניתן לשלוח את Circuit ה-ISA המתקבל לביצוע על ה-Backend (שלב 3 של תבנית Qiskit).

שלב 3: ביצוע על חומרה קוונטית

from qiskit_ibm_runtime import EstimatorV2 as Estimator

estimator = Estimator(backend)
job = estimator.run([(isa_circuit, isa_observable)])
pub_result = job.result()[0]

שלב 4: שחזור

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

pub_result.data.evs[()]
np.float64(0.047998046875000006)