ד וגמאות ויישומים
בשיעור זה נחקור כמה דוגמאות לאלגוריתמים וריאציוניים וכיצד ליישם אותם:
- כיצד לכתוב אלגוריתם וריאציוני מותאם אישית
- כיצד ליישם אלגוריתם וריאציוני למציאת ערכי עצמי מינימליים
- כיצד להשתמש באלגוריתמים וריאציוניים לפתרון מקרי שימוש יישומיים
שימו לב שמסגרת תבניות Qiskit ניתנת ליישום בכל הבעיות שנציג כאן. אולם, כדי להימנע מחזרות מיותרות, נציין במפורש את שלבי המסגרת רק בדוגמה אחת, שתרוץ על חומרה אמיתית.
הגדרות בעיות
נדמיין שאנחנו רוצים להשתמש באלגוריתם וריאציוני כדי למצוא את ערך העצמי של האובייקט הנצפה הבא:
לאובייקט נצפה זה יש את ערכי העצמי הבאים:
ומצבי עצמי:
# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-ibm-runtime rustworkx scipy
from qiskit.quantum_info import SparsePauliOp
observable_1 = SparsePauliOp.from_list([("II", 2), ("XX", -2), ("YY", 3), ("ZZ", -3)])
VQE מותאם אישית
תחילה נחקור כיצד לבנות מופע VQE ידנית כדי למצוא את ערך העצמי הנמוך ביותר עבור . זה ישלב מגוון טכניקות שסקרנו לאורך הקורס.
def cost_func_vqe(params, ansatz, hamiltonian, estimator):
"""Return estimate of energy from estimator
Parameters:
params (ndarray): Array of ansatz parameters
ansatz (QuantumCircuit): Parameterized ansatz circuit
hamiltonian (SparsePauliOp): Operator representation of Hamiltonian
estimator (Estimator): Estimator primitive instance
Returns:
float: Energy estimate
"""
pub = (ansatz, hamiltonian, params)
cost = estimator.run([pub]).result()[0].data.evs
return cost
from qiskit.circuit.library.n_local import n_local
from qiskit import QuantumCircuit
import numpy as np
reference_circuit = QuantumCircuit(2)
reference_circuit.x(0)
variational_form = n_local(
num_qubits=2,
rotation_blocks=["rz", "ry"],
entanglement_blocks="cx",
entanglement="linear",
reps=1,
)
raw_ansatz = reference_circuit.compose(variational_form)
raw_ansatz.decompose().draw("mpl")

נתחיל לנפות שגיאות על סימולטורים מקומיים.
from qiskit.primitives import StatevectorEstimator as Estimator
from qiskit.primitives import StatevectorSampler as Sampler
estimator = Estimator()
sampler = Sampler()
כעת נגדיר קבוצה התחלתית של פרמטרים:
x0 = np.ones(raw_ansatz.num_parameters)
print(x0)
[1. 1. 1. 1. 1. 1. 1. 1.]
אנחנו יכולים למזער את פונקציית העלות הזו כדי לחשב פרמטרים אופטימלי ים
# SciPy minimizer routine
from scipy.optimize import minimize
import time
start_time = time.time()
result = minimize(
cost_func_vqe,
x0,
args=(raw_ansatz, observable_1, estimator),
method="COBYLA",
options={"maxiter": 1000, "disp": True},
)
end_time = time.time()
execution_time = end_time - start_time
Return from COBYLA because the trust region radius reaches its lower bound.
Number of function values = 103 Least value of F = -5.999999998357189
The corresponding X is:
[2.27483579e+00 8.37593091e-01 1.57080508e+00 5.82932911e-06
2.49973063e+00 6.41884255e-01 6.33686904e-01 6.33688223e-01]
result
message: Return from COBYLA because the trust region radius reaches its lower bound.
success: True
status: 0
fun: -5.999999998357189
x: [ 2.275e+00 8.376e-01 1.571e+00 5.829e-06 2.500e+00
6.419e-01 6.337e-01 6.337e-01]
nfev: 103
maxcv: 0.0
מכיוון שבעיית הצעצוע הזו משתמשת בשני Qubit בלבד, נוכל לאמת זאת באמצעות פותר ערכי עצמי מלינארית של NumPy.
from numpy.linalg import eigvalsh
solution_eigenvalue = min(eigvalsh(observable_1.to_matrix()))
print(f"""Number of iterations: {result.nfev}""")
print(f"""Time (s): {execution_time}""")
print(
f"Percent error: {100*abs((result.fun - solution_eigenvalue)/solution_eigenvalue):.2e}"
)
Number of iterations: 103
Time (s): 0.4394676685333252
Percent error: 2.74e-08
כפי ש ניתן לראות, התוצאה קרובה מאוד לאידיאל.
ניסוי לשיפור מהירות ודיוק
הוספת מצב ייחוס
בדוגמה הקודמת לא השתמשנו באף אופרטור ייחוס . כעת נחשוב כיצד ניתן להגיע למצב העצמי האידיאלי . שקלו את ה-Circuit הבא.
from qiskit import QuantumCircuit
ideal_qc = QuantumCircuit(2)
ideal_qc.h(0)
ideal_qc.cx(0, 1)
ideal_qc.draw("mpl")
נוכל לאמת במהירות שה-Circuit הזה מספק לנו את המצב הרצוי.
from qiskit.quantum_info import Statevector
Statevector(ideal_qc)
Statevector([0.70710678+0.j, 0. +0.j, 0. +0.j,
0.70710678+0.j],
dims=(2, 2))
כעת שראינו כיצד נראה Circuit המכין את מצב הפתרון, סביר להשתמש ב-Gate הדמארד כ-Circuit ייחוס, כך שה-ansatz המלא יהיה:
reference = QuantumCircuit(2)
reference.h(0)
reference.cx(0, 1)
# Include barrier to separate reference from variational form
reference.barrier()
ref_ansatz = variational_form.decompose().compose(reference, front=True)
ref_ansatz.draw("mpl")

עבור Circuit חדש זה, הפתרון האידיאלי ניתן להשגה כאשר כל הפרמטרים מוגדרים ל-, מה שמאשר שבחירת ה-Circuit כייחוס היא סבירה.
כעת נשווה את מספר חישובי פונקציית העלות, איטרציות המייעל והזמן שנלקח אל מול הניסיון הקודם.
import time
start_time = time.time()
ref_result = minimize(
cost_func_vqe, x0, args=(ref_ansatz, observable_1, estimator), method="COBYLA"
)
end_time = time.time()
execution_time = end_time - start_time
שימוש בפרמטרים האופטימליים שלנו לחישוב ערך העצמי המינימלי:
experimental_min_eigenvalue_ref = cost_func_vqe(
ref_result.x, ref_ansatz, observable_1, estimator
)
print(experimental_min_eigenvalue_ref)
-5.999999996759607
print("ADDED REFERENCE STATE:")
print(f"""Number of iterations: {ref_result.nfev}""")
print(f"""Time (s): {execution_time}""")
print(
f"Percent error: {100*abs((experimental_min_eigenvalue_ref - solution_eigenvalue)/solution_eigenvalue):.2e}"
)
ADDED REFERENCE STATE:
Number of iterations: 127
Time (s): 0.5620882511138916
Percent error: 5.40e-08
בהתאם למערכת הספציפית שלכם, ייתכן שזה יגרום לשיפור במהירות או בדיוק בדוגמה בקנה מידה קטן מאוד זה, וייתכן שלא. הנקודה היא שהתחלה עם מצבי ייחוס המונעים פיזיקלית הופכת חשובה יותר ויותר לשיפור המהירות והדיוק ככל שהבעיות גדלות.
שינוי נקודת ההתחלה
כעת שראינו את השפעת הוספת מצב הייחוס, נבחן מה קורה כאשר בוחרים נקודות התחלה שונות. בפרט נשתמש ב- וב-.
זכרו שכפי שנדון כאשר הוצג מצב הייחוס, הפתרון האידיאלי יימצא כאשר כל הפרמטרים הם , כך שנקודת ההתחלה הראשונה אמורה לדרוש פחות חישובים.
import time
start_time = time.time()
x0 = [0, 0, 0, 0, 6, 0, 0, 0]
x0_1_result = minimize(
cost_func_vqe, x0, args=(raw_ansatz, observable_1, estimator), method="COBYLA"
)
end_time = time.time()
execution_time = end_time - start_time
print("INITIAL POINT 1:")
print(f"""Number of iterations: {x0_1_result.nfev}""")
print(f"""Time (s): {execution_time}""")
INITIAL POINT 1:
Number of iterations: 108
Time (s): 0.4492197036743164
כוונון נקודת ההתחלה ל-:
import time
start_time = time.time()
x0 = 6 * np.ones(raw_ansatz.num_parameters)
x0_2_result = minimize(
cost_func_vqe, x0, args=(raw_ansatz, observable_1, estimator), method="COBYLA"
)
end_time = time.time()
execution_time = end_time - start_time
print("INITIAL POINT 2:")
print(f"""Number of iterations: {x0_2_result.nfev}""")
print(f"""Time (s): {execution_time}""")
INITIAL POINT 2:
Number of iterations: 107
Time (s): 0.40889453887939453
על ידי ניסוי עם נקודות התחלה שונות, ייתכן שתצליחו להגיע להתכנסות מהר יותר ועם פחות חישובי פונקציה.
ניסוי עם מייעלים שונים
אנחנו יכולים לשנות את המייעל באמצעות הארגומנט method של minimize של SciPy, עם אפשרויות נוספות שנמצאות כאן. במקור השתמשנו במייעל מוגבל (COBYLA). בדוגמה זו נחקור שימוש במייעל לא-מוגבל (BFGS) במקום
import time
start_time = time.time()
result = minimize(
cost_func_vqe, x0, args=(raw_ansatz, observable_1, estimator), method="BFGS"
)
end_time = time.time()
execution_time = end_time - start_time
print("CHANGED TO BFGS OPTIMIZER:")
print(f"""Number of iterations: {result.nfev}""")
print(f"""Time (s): {execution_time}""")
CHANGED TO BFGS OPTIMIZER:
Number of iterations: 117
Time (s): 0.31656408309936523
דוגמת VQD
כאן אנחנו מממשים את מסגרת הדפוסים של Qiskit באופן מפורש.
שלב 1: מיפוי קלטים קלאסיים לבעיה קוונטית
במקום לחפש רק את ערך העצמי הנמוך ביותר של האופרטורים שלנו, כאן נחפש את כל הערכים (כאשר ).
זכרו שפונקציות העלות של VQD הן:
זה חשוב במיוחד מכיוון שיש להעביר וקטור (במקרה זה ) כארגומנט בעת הגדרת אובייקט ה-VQD.
כמו כן, במימוש VQD של Qiskit, במקום להשתמש באופרטורים האפקטיביים שתוארו במחברת הקודמת, הנאמנויות מחושבות ישירות באמצעות האלגוריתם ComputeUncompute, שמשתמש ב-Sampler primitive כדי לדגום את ההסתברות לקבל עבור ה-Circuit
. זה עובד בדיוק כי ההסתברות הזו היא
ansatz = n_local(
num_qubits=2,
rotation_blocks=["ry", "rz"],
entanglement_blocks="cz",
# entanglement="linear",
reps=1,
)
ansatz.decompose().draw("mpl")

נתחיל בבחינת האופרטור הבא:
לאופרטור זה יש את ערכי העצמי הבאים:
ומצבי עצמי:
from qiskit.quantum_info import SparsePauliOp
observable_2 = SparsePauliOp.from_list([("II", 2), ("XX", -3), ("YY", 2), ("ZZ", -4)])
נשתמש בפונקציה הבאה לחישוב עונש החפיפה. שימו לב שזה עדיין חלק ממיפוי הבעיה למעגלים קוונטיים. אולם, כפי שנדון בשיעור הקודם, פונקציה זו מחשבת את החפיפה בין ה-Circuit הווריאציוני הנוכחי לבין ה-Circuit האופטימלי ממצב עם אנרגיה/עלות נמוכה יותר שהתקבל קודם. ה-Circuit החדש שנוצר גם הוא חייב להיות מותאם (transpiled) להרצה על חומרה אמיתית. ראינו פונקציה זו לפני כן, בשימוש על סימולטור. כאן עלינו כבר לקחת בחשבון את ה-Transpile והאופטימיזציה הנלווית לשימוש ב-Backend אמיתי, ומכאן השורות סביב if realbackend == 1. זה מערבב קצת את שלב 2, אך נציין את שלב 2 בפירוש בהמשך.
import numpy as np
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
def calculate_overlaps(
ansatz, prev_circuits, parameters, sampler, realbackend, backend
):
def create_fidelity_circuit(circuit_1, circuit_2):
if len(circuit_1.clbits) > 0:
circuit_1.remove_final_measurements()
if len(circuit_2.clbits) > 0:
circuit_2.remove_final_measurements()
circuit = circuit_1.compose(circuit_2.inverse())
circuit.measure_all()
return circuit
overlaps = []
for prev_circuit in prev_circuits:
fidelity_circuit = create_fidelity_circuit(ansatz, prev_circuit)
if realbackend == 1:
pm = generate_preset_pass_manager(backend=backend, optimization_level=3)
fidelity_circuit = pm.run(fidelity_circuit)
sampler_job = sampler.run([(fidelity_circuit, parameters)])
meas_data = sampler_job.result()[0].data.meas
counts_0 = meas_data.get_int_counts().get(0, 0)
shots = meas_data.num_shots
overlap = counts_0 / shots
overlaps.append(overlap)
return np.array(overlaps)
כעת נוסיף את פונקציית העלות של VQD. שימו לב שבהשוואה לשיעור הקודם, יש לנו כעת שני ארגומנטים נוספים (realbackend ו-backend) שיעזרו לנו עם ה-Transpile כשנשתמש ב-Backends אמיתיים.
def cost_func_vqd(
parameters,
ansatz,
prev_states,
step,
betas,
estimator,
sampler,
hamiltonian,
realbackend,
backend,
):
estimator_job = estimator.run([(ansatz, hamiltonian, [parameters])])
total_cost = 0
if step > 1:
overlaps = calculate_overlaps(
ansatz, prev_states, parameters, sampler, realbackend, backend
)
total_cost = np.sum(
[np.real(betas[state] * overlap) for state, overlap in enumerate(overlaps)]
)
estimator_result = estimator_job.result()[0]
value = estimator_result.data.evs[0] + total_cost
return value
שוב, נשתמש בסימולטורים לצורכי דיבוג, ואז נעבור לחומרה אמיתית.
from qiskit.primitives import StatevectorSampler
from qiskit.primitives import StatevectorEstimator
sampler = StatevectorSampler(default_shots=4092)
estimator = StatevectorEstimator()
כאן אנחנו מגדירים את מספר המצבים שאנחנו רוצים לחשב, את העונשים, ומערך פרמטרים התחלתיים, x0.
from qiskit.quantum_info import SparsePauliOp
k = 4
betas = [50, 60, 40]
x0 = np.ones(8)
כעת נבדוק את האלגוריתם באמצעות סימולטורים:
from scipy.optimize import minimize
prev_states = []
prev_opt_parameters = []
eigenvalues = []
realbackend = 0
for step in range(1, k + 1):
if step > 1:
prev_states.append(ansatz.assign_parameters(prev_opt_parameters))
result = minimize(
cost_func_vqd,
x0,
args=(
ansatz,
prev_states,
step,
betas,
estimator,
sampler,
observable_2,
realbackend,
None,
),
method="COBYLA",
options={"maxiter": 200, "tol": 0.000001},
)
print(result)
prev_opt_parameters = result.x
eigenvalues.append(result.fun)
message: Return from COBYLA because the trust region radius reaches its lower bound.
success: True
status: 0
fun: -6.9999999999996
x: [ 1.571e+00 1.571e+00 2.519e+00 2.100e+00 1.242e+00
6.935e-01 2.298e+00 1.991e+00]
nfev: 151
maxcv: 0.0
message: Return from COBYLA because the trust region radius reaches its lower bound.
success: True
status: 0
fun: 3.698974255258432
x: [ 1.269e+00 1.109e+00 1.080e+00 1.200e+00 1.094e+00
1.163e+00 9.752e-01 9.519e-01]
nfev: 103
maxcv: 0.0
message: Return from COBYLA because the trust region radius reaches its lower bound.
success: True
status: 0
fun: 4.731320121938101
x: [ 1.533e+00 2.451e+00 2.526e+00 2.406e+00 1.968e+00
2.105e+00 8.537e-01 8.442e-01]
nfev: 110
maxcv: 0.0
message: Return from COBYLA because the trust region radius reaches its lower bound.
success: True
status: 0
fun: 7.008239313655201
x: [ 4.150e+00 2.120e+00 3.495e+00 7.262e-01 1.953e+00
-1.982e-01 3.263e-01 2.563e+00]
nfev: 126
maxcv: 0.0
eigenvalues
[np.float64(-6.9999999999996),
np.float64(3.698974255258432),
np.float64(4.731320121938101),
np.float64(7.008239313655201)]
התוצאות האלה קרובות למדי לתוצאות הצפויות, פרט לשגיאת קירוב ופאזה גלובלית. אפשר לכוונן את הסבילות של המייטב הקלאסי ו/או את העונשים על חפיפת ה-statevector כדי לקבל ערכים מדויקים יותר.
solution_eigenvalues = [-7, 3, 5, 7]
for index, experimental_eigenvalue in enumerate(eigenvalues):
solution_eigenvalue = solution_eigenvalues[index]
print(
f"Percent error: {abs((experimental_eigenvalue - solution_eigenvalue)/solution_eigenvalue):.2e}"
)
Percent error: 5.71e-14
Percent error: 2.33e-01
Percent error: 5.37e-02
Percent error: 1.18e-03
שינוי ה-betas
כפי שצוין בשיעור הקודם, הערכים של צריכים להיות גדולים מההפרש בין הערכים העצמיים. בואו נראה מה קורה כשהם לא עומדים בתנאי הזה עם
עם ערכים עצמיים
from qiskit.quantum_info import SparsePauliOp
k = 4
betas = np.ones(3)
x0 = np.zeros(8)
from scipy.optimize import minimize
prev_states = []
prev_opt_parameters = []
eigenvalues = []
realbackend = 0
for step in range(1, k + 1):
if step > 1:
prev_states.append(ansatz.assign_parameters(prev_opt_parameters))
result = minimize(
cost_func_vqd,
x0,
args=(
ansatz,
prev_states,
step,
betas,
estimator,
sampler,
observable_2,
realbackend,
None,
),
method="COBYLA",
options={"tol": 0.01, "maxiter": 200},
)
print(result)
prev_opt_parameters = result.x
eigenvalues.append(result.fun)
message: Return from COBYLA because the trust region radius reaches its lower bound.
success: True
status: 0
fun: -6.999916534745094
x: [ 1.568e+00 -1.569e+00 1.385e-01 1.398e-01 -7.972e-01
7.835e-01 -2.375e-01 4.539e-02]
nfev: 125
maxcv: 0.0
message: Return from COBYLA because the trust region radius reaches its lower bound.
success: True
status: 0
fun: -1.515139929812874
x: [-5.317e-04 -2.514e-03 1.016e+00 9.998e-01 3.890e-04
1.772e-04 1.568e-04 8.497e-04]
nfev: 35
maxcv: 0.0
message: Return from COBYLA because the trust region radius reaches its lower bound.
success: True
status: 0
fun: -0.509948114293115
x: [-3.796e-03 8.853e-03 3.015e-04 9.997e-01 6.271e-04
-2.554e-03 1.017e-04 2.766e-04]
nfev: 37
maxcv: 0.0
message: Return from COBYLA because the trust region radius reaches its lower bound.
success: True
status: 0
fun: 0.4914672235935682
x: [-7.178e-03 -8.652e-03 1.125e+00 -5.428e-02 -1.586e-03
2.031e-03 -3.462e-03 5.734e-03]
nfev: 35
maxcv: 0.0
solution_eigenvalues = [-7, 3, 5, 7]
for index, experimental_eigenvalue in enumerate(eigenvalues):
solution_eigenvalue = solution_eigenvalues[index]
print(
f"Percent error: {abs((experimental_eigenvalue - solution_eigenvalue)/solution_eigenvalue):.2e}"
)
Percent error: 1.19e-05
Percent error: 1.51e+00
Percent error: 1.10e+00
Percent error: 9.30e-01
הפעם, המייעל מחזיר את אותה מצב כפתרון מוצע לכל המצבים העצמיים — וזה כמובן שגוי. זה קורה כי ה-betas היו קטנים מדי כדי להעניש את המצב העצמי המינימלי בפונקציות העלות הבאות. לכן, הוא לא הוצא ממרחב החיפוש האפקטיבי באיטרציות מאוחרות יותר של האלגוריתם, ותמיד נבחר כפתרון הטוב ביותר האפשרי.
אנחנו ממליצים להתנסות עם הערכים של ולוודא שהם גדולים מההפרש בין הערכים העצמיים.
שלב 2: אופטימיזציה של הבעיה להרצה קוונטית
כדי להריץ את זה על חומרה אמיתית, עלינו לאמול את המעגלים הקוונטיים למחשב הקוונטי שבחרנו. לצורכינו כאן, פשוט נשתמש ב-Backend הפחות עמוס.
from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_ibm_runtime import Session, EstimatorOptions
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
# Or use a specific backend
# backend = service.backend("ibm_brisbane")
print(backend)
<IBMBackend('ibm_brisbane')>
נבצע טרנספילציה של ה-Circuit שלנו באמצעות מנהל pass מוגדר-מראש ורמת אופטימיזציה 3.
pm = generate_preset_pass_manager(backend=backend, optimization_level=3)
isa_ansatz = pm.run(ansatz)
isa_observable = observable_2.apply_layout(layout=isa_ansatz.layout)
שלב 3: הרצה באמצעות פרימיטיבים של Qiskit
תוך דאגה לאפס את ה-betas שלנו לערכים גבוהים מספיק, אנחנו יכולים עכשיו להריץ את החישוב שלנו על חומרה קוונטית אמיתית.
# Estimated compute resource usage: 25 minutes. Benchmarked at 24 min, 30 sec on an Eagle r3 processor on 5-30-24
k = 2
betas = [30, 50, 80]
x0 = np.zeros(8)
real_prev_states = []
real_prev_opt_parameters = []
real_eigenvalues = []
realbackend = 1
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
sampler = Sampler(mode=session)
for step in range(1, k + 1):
if step > 1:
real_prev_states.append(isa_ansatz.assign_parameters(prev_opt_parameters))
result = minimize(
cost_func_vqd,
x0,
args=(
isa_ansatz,
real_prev_states,
step,
betas,
estimator,
sampler,
isa_observable,
realbackend,
backend,
),
method="COBYLA",
options={"maxiter": 200},
)
print(result)
real_prev_opt_parameters = result.x
real_eigenvalues.append(result.fun)
session.close()
print(real_eigenvalues)
שלב 4: עיבוד לאחר, החזרת תוצאה בפורמט קלאסי
הפלט שלנו דומה מבנית למה שנדון בשיעורים ובדוגמאות הקודמים. אבל יש משהו בעייתי בתוצאות לעיל, שממנו אפשר להפיק מסר אזהרה בהקשר של מצבים מעוררים. כדי להגביל את זמן החישוב המשמש בדוגמת לימוד זו, הגדרנו מספר מקסימלי של איטרציות עבור המייעל הקלאסי שהיה נמוך מדי: 200 איטרציות. חישוב קודם לעיל, על סימולטור, לא התכנס ב-200 איטרציות. כאן, שלנו כן התכנס... אבל לאיזו סובלנות? לא ציינו סובלנות ש-COBYLA יחשיב את עצמו "מתכנס". מבט על ערך הפונקציה והשוואה עם הרצות קודמות מגלה לנו ש-COBYLA לא היה קרוב להתכנסות לדיוק שאנחנו דורשים.
יש עוד בעיה: האנרגיה של המצב המעורר הראשון נראית נמוכה מהאנרגיה של מצב היסוד! נסו להסביר כיצד זה יכול לקרות. רמז: זה קשור לנקודת ההתכנסות שזה עתה הזכרנו. התנהגות זו מוסברת בפירוט למטה לאחר שמיישמים את VQD על מולקולת ה-H2.
כימיה קוונטית: פותר אנרגיית מצב יסוד ומצבים מעוררים
המטרה שלנו היא למזער את ערך הציפייה של האובזרבבל המייצג אנרגיה (ההמילטוניאן ):
from qiskit.quantum_info import SparsePauliOp
from qiskit.circuit.library import efficient_su2
H2_op = SparsePauliOp.from_list(
[
("II", -1.052373245772859),
("IZ", 0.39793742484318045),
("ZI", -0.39793742484318045),
("ZZ", -0.01128010425623538),
("XX", 0.18093119978423156),
]
)
chem_ansatz = efficient_su2(H2_op.num_qubits)
chem_ansatz.decompose().draw("mpl")

from qiskit import QuantumCircuit
def cost_func_vqe(params, ansatz, hamiltonian, estimator):
"""Return estimate of energy from estimator
Parameters:
params (ndarray): Array of ansatz parameters
ansatz (QuantumCircuit): Parameterized ansatz circuit
hamiltonian (SparsePauliOp): Operator representation of Hamiltonian
estimator (Estimator): Estimator primitive instance
Returns:
float: Energy estimate
"""
pub = (ansatz, hamiltonian, params)
cost = estimator.run([pub]).result()[0].data.evs
# cost = estimator.run(ansatz, hamiltonian, parameter_values=params).result().values[0]
return cost
עכשיו נגדיר קבוצה התחלתית של פרמטרים:
import numpy as np
x0 = np.ones(chem_ansatz.num_parameters)
אפשר למזער את פונקציית העלות הזאת כדי לחשב פרמטרים אופטימליים, ונוכל לבדוק את הקוד שלנו תחילה באמצעות סימולטור מקומי.
from qiskit.primitives import StatevectorEstimator as Estimator
from qiskit.primitives import StatevectorSampler as Sampler
estimator = Estimator()
sampler = Sampler()
# SciPy minimizer routine
from scipy.optimize import minimize
import time
start_time = time.time()
result = minimize(
cost_func_vqe, x0, args=(chem_ansatz, H2_op, estimator), method="COBYLA"
)
end_time = time.time()
execution_time = end_time - start_time
result
message: Optimization terminated successfully.
success: True
status: 1
fun: -1.857275029048451
x: [ 7.326e-01 1.354e+00 ... 1.040e+00 1.508e+00]
nfev: 242
maxcv: 0.0
הערך המינימלי של פונקציית העלות (-1.857...) הוא אנרגיית מצב היסוד של מולקולת H2, ביחידות של הארטרי.
מצבים מעוררים
אפשר גם להשתמש ב-VQD כדי לפתור עבור מצבות כולל (מצב היסוד והמצב המעורר הראשון).
from qiskit.quantum_info import SparsePauliOp
import numpy as np
k = 2
betas = [33, 33]
# x0 = np.zeros(ansatz.num_parameters)
x0 = [
1.164e00,
-2.438e-01,
9.358e-04,
6.745e-02,
1.990e00,
9.810e-02,
6.154e-01,
5.454e-01,
]
נוסיף את חישוב החפיפה שלנו:
from scipy.optimize import minimize
prev_states = []
prev_opt_parameters = []
eigenvalues = []
realbackend = 0
for step in range(1, k + 1):
if step > 1:
prev_states.append(ansatz.assign_parameters(prev_opt_parameters))
result = minimize(
cost_func_vqd,
x0,
args=(
ansatz,
prev_states,
step,
betas,
estimator,
sampler,
H2_op,
realbackend,
None,
),
method="COBYLA",
options={"tol": 0.001, "maxiter": 2000},
)
print(result)
prev_opt_parameters = result.x
eigenvalues.append(result.fun)
message: Optimization terminated successfully.
success: True
status: 1
fun: -1.8572671093941977
x: [ 1.164e+00 -2.437e-01 2.118e-03 6.448e-02 1.990e+00
9.870e-02 6.167e-01 5.476e-01]
nfev: 58
maxcv: 0.0
message: Optimization terminated successfully.
success: True
status: 1
fun: -1.0322873777662176
x: [ 3.205e+00 1.502e+00 1.699e+00 -1.107e-02 3.086e+00
1.530e+00 4.445e-02 7.013e-02]
nfev: 99
maxcv: 0.0
eigenvalues
[-1.8572671093941977, -1.0322873777662176]
חומרה אמיתית ואזהרה אחרונה
כדי להריץ את זה על חומרה אמיתית, עלינו לאופטימיזציה את מעגלי ה-Quantum עבור מחשב הקוונטי שבחרנו. לצורכינו כאן, פשוט נשתמש ב-Backend הפחות עמוס.
from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_ibm_runtime import Session, EstimatorOptions
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
נשתמש ב-pass manager מוגדר מראש לטרנספילציה, ונבצע אופטימיזציה מקסימלית של המעגל שלנו באמצעות רמת אופטימיזציה 3.
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
pm = generate_preset_pass_manager(backend=backend, optimization_level=3)
isa_ansatz = pm.run(ansatz)
isa_observable = H2_op.apply_layout(layout=isa_ansatz.layout)
מכיוון ש-VQD הוא בעל אופי איטרטיבי מאוד, נבצע את כל השלבים בתוך Session של Runtime, כך שהמשימות שלנו יכנסו לתור רק בהתחלה, ולא בין כל עדכון פרמטרים. שום דבר אחר לא משתנה בתחביר עבור פונקציית העלות או ה-Estimator.
x0 = [
1.306e00,
-2.284e-01,
6.913e-02,
-2.530e-02,
1.849e00,
7.433e-02,
6.366e-01,
5.600e-01,
]
# Estimated hardware usage: 20 min benchmarked on an Eagle r3 processor on 5-30-24
real_prev_states = []
real_prev_opt_parameters = []
real_eigenvalues = []
realbackend = 1
estimator_options = EstimatorOptions(resilience_level=1, default_shots=4096)
with Session(backend=backend) as session:
estimator = Estimator(mode=session)
sampler = Sampler(mode=session)
for step in range(1, k + 1):
if step > 1:
real_prev_states.append(
isa_ansatz.assign_parameters(real_prev_opt_parameters)
)
result = minimize(
cost_func_vqd,
x0,
args=(
isa_ansatz,
real_prev_states,
step,
betas,
estimator,
sampler,
isa_observable,
realbackend,
backend,
),
method="COBYLA",
options={"tol": 0.001, "maxiter": 300},
)
print(result)
real_prev_opt_parameters = result.x
real_eigenvalues.append(result.fun)
session.close()
print(real_eigenvalues)
אנרגיית מצב היסוד שהתקבלה (-1.83 הארטרי) אינה רחוקה מדי מהערך הנכון (-1.85 הארטרי). אולם, אנרגיית המצב המעורר חורגת למדי. זה דומה להתנהגות השגויה שראינו מוקדם יותר בשיעור הזה. האנרגיה המדווחת עבור המצב המעורר כמעט זהה לזו של מצב היסוד. במקרה הקודם, ראינו אפילו אנרגיית מצב מעורר שהייתה נמוכה מאנרגיית מצב היסוד המדווחת.
אין זה אפשרי שחישוב וריאציוני יניב אנרגיה הנמוכה מאנרגיית מצב היסוד האמיתית. במקרה הקודם, אנרגיית מצב היסוד שקיבלנו לא הייתה קרובה במיוחד לאנרגיית מצב היסוד האמיתית. מכיוון שלא קיבלנו את אנרגיית מצב היסוד האמיתית במקרה ההוא, אין סתירה. במקרה הנוכחי, אנרגיית מצב היסוד הייתה קרובה למדי לערך הנכון, ובכל זאת אנרגיית המצב המעורר נראית קרובה באופן מוזר לאותו ערך.
כדי להבין טוב יותר כיצד זה קרה, נזכיר שהדרך שבה אנו מוצאים מצב מעורר היא על ידי דרישה שהמצב הווריאציוני יהיה אורתוגונלי למצב היסוד (באמצעות מעגלי החפיפה וערמי הקנס). אם לא הצלחנו לקבל אנרגיית מצב יסוד מדויקת (או שסטינו בכמה אחוזים), אז גם לא הצלחנו לקבל וקטור מצב יסוד מדויק! לכן, כאשר דרשנו שהמצב המעורר יהיה אורתוגונלי למצב הראשון שמצאנו, לא כפינו אורתוגונליות עם מצב היסוד האמיתי, אלא עם קירוב כלשהו שלו (לעיתים קירוב גרוע). לפיכך, המצב המעורר לא נאלץ להיות אורתוגונלי למצב היסוד האמיתי, ואומדני האנרגיה שלנו עבור המצבים המעוררים היו למעשה קרובים מאוד לאנרגיית מצב היסוד.
זה תמיד יהיה גורם לדאגה ב-VQD. אבל באופן עקרוני, ניתן לתקן זאת על ידי הגדלת מספר האיטרציות המרבי עבור האופטימיזר הקלאסי, הטלת סובלנות נמוכה יותר עבור האופטימיזר הקלאסי, ואולי גם ניסיון של ansatz שונה אם אנחנו בדרך כלל מפספסים את מצב היסוד האמיתי. כפי שראינו, ייתכן שיהיה צורך גם לשנות את ערמי החפיפה (betas). אבל זה באמת נושא נפרד. אף קנס על חפיפה לא ישמור אותך הרחק ממצב היסוד האמיתי, אם לא מצאת אומדן טוב מאוד של מצב היסוד האמיתי עבור מעגל החפיפה.
אופטימיזציה: Max-Cut
בעיית החיתוך המקסימלי (Max-Cut) היא בעיית אופטימיזציה קומבינטורית שעוסקת בחלוקת צמתי גרף לשתי קבוצות זרות כך שמספר הקשתות בין שתי הקבוצות יהיה מקסימלי. באופן פורמלי יותר, בהינתן גרף לא מכוון , כאשר הוא קבוצת הצמתים ו- הוא קבוצת הקשתות, בעיית Max-Cut מבקשת לחלק את הצמתים לשתי תת-קבוצות זרות, ו-, כך שמספר הקשתות עם קצה אחד ב- וקצה שני ב- יהיה מקסימלי.
ניתן להחיל את Max-Cut לפתרון בעיות שונות, כגון אשכול, תכנון רשתות ומעברי פאזה. נתחיל ביצירת גרף הבעיה:
import rustworkx as rx
from rustworkx.visualization import mpl_draw
n = 4
G = rx.PyGraph()
G.add_nodes_from(range(n))
# The edge syntax is (start, end, weight)
edges = [(0, 1, 1.0), (0, 2, 1.0), (0, 3, 1.0), (1, 2, 1.0), (2, 3, 1.0)]
G.add_edges_from(edges)
mpl_draw(
G, pos=rx.shell_layout(G), with_labels=True, edge_labels=str, node_color="#1192E8"
)
ניתן לבטא בעיה זו כבעיית אופטימיזציה בינארית. לכל צומת , כאשר הוא מספר הצמתים בגרף (במקרה זה ), נשקול את המשתנה הבינארי . משתנה זה יקבל את הערך אם צומת נמצא באחת הקבוצות שנסמן ב-, ו- אם הוא בקבוצה השנייה שנסמן ב-. כמו כן נסמן ב- (איבר של מטריצת הסמיכות ) את משקל הקשת מצומת לצומת . מכיוון שהגרף אינו מכוון, . לאחר מכן ניתן לנסח את הבעיה כמקסום פונקציית העלות הבאה:
כדי לפתור בעיה זו עם מחשב קוונטי, נבטא את פונקציית העלות כערך הציפייה של אופרטור צפייה. עם זאת, אופרטורי הצפייה שקיסקיט תומך בהם באופן מובנה מורכבים מאופרטורי פאולי, שלהם ערכים עצמיים ו- במקום ו-. לכן נבצע את שינוי המשתנים הבא:
כאשר . ניתן להשתמש במטריצת הסמיכות לגישה נוחה למשקלי כל הקשתות. זה ישמש אותנו לקבלת פונקציית העלות:
מכך נובע:
לכן פונקציית העלות החדשה שאנו רוצים למקסם היא:
יתרה מזאת, המגמה הטבעית של מחשב קוונטי היא למצוא מינימום (בדרך כלל אנרגיה הנמוכה ביותר) ולא מקסימום, לכן במקום למקסם את נמזם:
עכשיו שיש לנו פונקציית עלות למינוי שמשתניה יכולים לקבל את הערכים ו-, ניתן לבצע את האנלוגיה הבאה עם פאולי :
במילים אחרות, ה משתנה יהיה שקול ל-Gate הפועל על Qubit . יתרה מזאת:
אז אופרטור הצפייה שנשקול הוא:
אליו נצטרך להוסיף את האיבר העצמאי לאחר מכן:
האופרטור הוא צירוף לינארי של איברים עם אופרטורי Z על צמתים המחוברים על ידי קשת (נזכור שה-Qubit ה-0 הוא הימני ביותר): . לאחר בניית האופרטור, ניתן בקלות לבנות את ה-ansatz לאלגוריתם QAOA באמצעות ה-Circuit QAOAAnsatz מספריית ה-Circuit של קיסקיט.
from qiskit.circuit.library import QAOAAnsatz
from qiskit.quantum_info import SparsePauliOp
max_hamiltonian = SparsePauliOp.from_list(
[("IIZZ", 1), ("IZIZ", 1), ("IZZI", 1), ("ZIIZ", 1), ("ZZII", 1)]
)
max_ansatz = QAOAAnsatz(max_hamiltonian, reps=2)
# Draw
max_ansatz.decompose(reps=3).draw("mpl")
# Sum the weights, and divide by 2
offset = -sum(edge[2] for edge in edges) / 2
print(f"""Offset: {offset}""")
Offset: -2.5
def cost_func(params, ansatz, hamiltonian, estimator):
"""Return estimate of energy from estimator
Parameters:
params (ndarray): Array of ansatz parameters
ansatz (QuantumCircuit): Parameterized ansatz circuit
hamiltonian (SparsePauliOp): Operator representation of Hamiltonian
estimator (Estimator): Estimator primitive instance
Returns:
float: Energy estimate
"""
pub = (ansatz, hamiltonian, params)
cost = estimator.run([pub]).result()[0].data.evs
# cost = estimator.run(ansatz, hamiltonian, parameter_values=params).result().values[0]
return cost
from qiskit.primitives import StatevectorEstimator as Estimator
from qiskit.primitives import StatevectorSampler as Sampler
estimator = Estimator()
sampler = Sampler()
כעת נגדיר קבוצה ראשונית של פרמטרים אקראיים:
import numpy as np
x0 = 2 * np.pi * np.random.rand(max_ansatz.num_parameters)
print(x0)
[6.0252949 0.58448176 2.15785731 1.13646074]
ניתן להשתמש בכל אופטימייזר קלאסי כדי למזם את פונקציית העלות. במערכת קוונטית אמיתית, אופטימייזר שנועד לנופי פונקציות עלות לא חלקים בדרך כלל עובד טוב יותר. כאן אנו משתמשים בשגרת COBYLA מ-SciPy דרך פונקציית minimize.
מכיוון שאנו מבצעים בצורה איטרטיבית קריאות רבות ל-Runtime, אנו משתמשים ב-Session כדי לבצע את כל הקריאות בתוך בלוק אחד. יתרה מזאת, עבור QAOA, הפתרון מקודד בהתפלגות הפלט של Circuit ה-ansatz הקשור עם הפרמטרים האופטימליים מהמינוי. לכן נזדקק ל-Primitive של Sampler, ונאתחל אותו עם אותה Session. ונריץ את שגרת המינוי שלנו:
result = minimize(
cost_func, x0, args=(max_ansatz, max_hamiltonian, estimator), method="COBYLA"
)
print(result)
message: Optimization terminated successfully.
success: True
status: 1
fun: -2.585287311689236
x: [ 7.332e+00 3.904e-01 2.045e+00 1.028e+00]
nfev: 80
maxcv: 0.0
וקטור הפתרון של זוויות הפרמטרים (x), כאשר מציבים אותו ב-Circuit ה-ansatz, מניב את חלוקת הגרף שחיפשנו.
eigenvalue = cost_func(result.x, max_ansatz, max_hamiltonian, estimator)
print(f"""Eigenvalue: {eigenvalue}""")
print(f"""Max-Cut Objective: {eigenvalue + offset}""")
Eigenvalue: -2.585287311689236
Max-Cut Objective: -5.085287311689235
from qiskit.result import QuasiDistribution
from qiskit.primitives import StatevectorSampler
sampler = StatevectorSampler()
# Assign solution parameters to ansatz
qc = max_ansatz.assign_parameters(result.x)
# Add measurements to our circuit
qc.measure_all()
# Sample ansatz at optimal parameters
# samp_dist = sampler.run(qc).result().quasi_dists[0]
shots = 1024
job = sampler.run([qc], shots=shots)
qc.decompose().draw("mpl")
data_pub = job.result()[0].data
bitstrings = data_pub.meas.get_bitstrings()
counts = data_pub.meas.get_counts()
quasi_dist = QuasiDistribution(
{outcome: freq / shots for outcome, freq in counts.items()}
)
probabilities = quasi_dist
# Close the session since we are now done with it
# session.close()
from qiskit.visualization import plot_distribution
plot_distribution(counts)
binary_string = max(counts.items(), key=lambda kv: kv[1])[0]
x = np.asarray([int(y) for y in reversed(list(binary_string))])
colors = ["r" if x[i] == 0 else "c" for i in range(n)]
mpl_draw(
G, pos=rx.shell_layout(G), with_labels=True, edge_labels=str, node_color=colors
)
סיכום
בשיעור זה למדת:
- כיצד לכתוב אלגוריתם וריאציוני מותאם אישית
- כיצד להחיל אלגוריתם וריאציוני למציאת ערכים עצמיים מינימליים
- כיצד לנצל אלגוריתמים וריאציוניים לפתרון מקרי שימוש יישומיים