תחילת העבודה עם חיתוך מעגלים באמצעות חיתוכי Gate
גרסאות חבילות
הקוד בדף זה פותח עם הדרישות הבאות. מומלץ להשתמש בגרסאות אלה או בגרסאות חדשות יותר.
qiskit[all]~=2.3.0
qiskit-ibm-runtime~=0.43.1
qiskit-aer~=0.17
qiskit-addon-cutting~=0.10.0
מדריך זה מדגים שתי דוגמות עבודה של חיתוכי Gate עם חבילת qiskit-addon-cutting. הדוגמה הראשונה מראה כיצד לצמצם את עומק המעגל (מספר הוראות המעגל) על ידי חיתוך Gate-ים מסבכים שאינם סמוכים, אשר אחרת היו גורמים לתקורה של SWAP בעת הTranspilation לחומרה. הדוגמה השנייה מכסה כיצד להשתמש בחיתוך Gate כדי לצמצם את רוחב המעגל (מספר ה-Qubit-ים) על ידי פיצול מעגל למספר מעגלים עם פחות Qubit-ים.
שתי הדוגמות ישתמשו ב-efficient_su2 ansatz ויבנו מחדש את אותו observable.
חיתוך Gate לצמצום עומק המעגל
תהליך העבודה הבא מצמצם את עומק המעגל על ידי חיתוך Gate-ים מרוחקים, ובכך מונע סדרה גדולה של Gate-י SWAP שאחרת היו מוכנסים.
התחל עם ה-efficient_su2 ansatz, עם סבכה "מעגלית" כדי להכניס Gate-ים מרוחקים.
# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-cutting qiskit-aer qiskit-ibm-runtime
import numpy as np
from qiskit.circuit.library import efficient_su2
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime.fake_provider import FakeManilaV2
from qiskit_ibm_runtime import SamplerV2, Batch
from qiskit_aer.primitives import EstimatorV2
from qiskit_addon_cutting import (
cut_gates,
partition_problem,
generate_cutting_experiments,
reconstruct_expectation_values,
)
circuit = efficient_su2(num_qubits=4, entanglement="circular")
circuit.assign_parameters([0.4] * len(circuit.parameters), inplace=True)
observable = SparsePauliOp(["ZZII", "IZZI", "-IIZZ", "XIXI", "ZIZZ", "IXIX"])
print(f"Observable: {observable}")
circuit.draw("mpl", scale=0.8)
Observable: SparsePauliOp(['ZZII', 'IZZI', 'IIZZ', 'XIXI', 'ZIZZ', 'IXIX'],
coeffs=[ 1.+0.j, 1.+0.j, -1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])
כל אחד מ-Gate-י CNOT בין Qubit-ים ו- מכניסים שני Gate-י SWAP לאחר הTranspilation (בהנחה שה-Qubit-ים מחוברים בקו ישר). כדי למנוע את הגדלת העומק הזו, אפשר להחליף את ה-Gate-ים המרוחקים האלה באובייקטים מסוג TwoQubitQPDGate באמצעות המתודה cut_gates(). פונקציה זו מחזירה גם רשימה של מופעי QPDBasis — אחד לכל פירוק.
# Find the indices of the distant gates
cut_indices = [
i
for i, instruction in enumerate(circuit.data)
if {circuit.find_bit(q)[0] for q in instruction.qubits} == {0, 3}
]
# Decompose distant CNOTs into TwoQubitQPDGate instances
qpd_circuit, bases = cut_gates(circuit, cut_indices)
qpd_circuit.draw("mpl", scale=0.8)
כעת שהוראות ה-Gate החתוך נוספו, לתת-הניסויים יהיה עומק קטן יותר לאחר הTranspilation בהשוואה למעגל המקורי. קטע הקוד הבא מייצר את תת-הניסויים באמצעות generate_cutting_experiments, שקולט את המעגל וה-observable לבנייה מחדש.
הארגומנט num_samples מציין כמה דגימות לשלוף מהתפלגות הסבירות המדומה ומשפיע על דיוק המקדמים המשמשים לבנייה מחדש. העברת אינסוף (np.inf) תבטיח שכל המקדמים יחושבו במדויק. קרא את תיעוד ה-API על יצירת משקלים ויצירת ניסויי חיתוך למידע נוסף.
לאחר יצירת תת-הניסויים, אפשר לבצע Transpilation עליהם ולהשתמש ב-primitive מסוג Sampler כדי לדגום את ההתפלגות ולבנות מחדש את ערכי ההציפייה המשוערים. בלוק הקוד הבא מייצר, מבצע Transpilation ומריץ את תת-הניסויים. לאחר מכן הוא בונה מחדש את התוצאות ומשווה אותן לערך ההציפייה המדויק.
# Generate the subexperiments and sampling coefficients
subexperiments, coefficients = generate_cutting_experiments(
circuits=qpd_circuit, observables=observable.paulis, num_samples=np.inf
)
# Set a backend to use and transpile the subexperiments
backend = FakeManilaV2()
pass_manager = generate_preset_pass_manager(
optimization_level=1, backend=backend
)
isa_subexperiments = pass_manager.run(subexperiments)
# Set up the Qiskit Runtime Sampler primitive, submit the subexperiments, and retrieve the results
sampler = SamplerV2(backend)
job = sampler.run(isa_subexperiments, shots=4096 * 3)
results = job.result()
# Reconstruct the results
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
observable.paulis,
)
# Apply the coefficients of the original observable
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)
estimator = EstimatorV2()
exact_expval = (
estimator.run([(circuit, observable, [0.4] * len(circuit.parameters))])
.result()[0]
.data.evs
)
print(
f"Reconstructed expectation value: {np.real(np.round(reconstructed_expval, 8))}"
)
print(f"Exact expectation value: {np.round(exact_expval, 8)}")
print(
f"Error in estimation: {np.real(np.round(reconstructed_expval-exact_expval, 8))}"
)
print(
f"Relative error in estimation: {np.real(np.round((reconstructed_expval-exact_expval) / exact_expval, 8))}"
)
Reconstructed expectation value: 0.49812826
Exact expectation value: 0.50497603
Error in estimation: -0.00684778
Relative error in estimation: -0.0135606
כדי לבנות מחדש את ערך ההציפייה בדיוק, יש להחיל את מקדמי ה-observable המקורי (שהם שונים מהמקדמים בפלט של generate_cutting_experiments()) על פלט הבנייה מחדש, מכיוון שמידע זה אבד כאשר ניסויי החיתוך נוצרו או כאשר ה-observable הורחב.
בדרך כלל ניתן להחיל מקדמים אלה באמצעות numpy.dot() כפי שמוצג למעלה.
חיתוך Gate לצמצום רוחב המעגל
חלק זה מדגים שימוש בחיתוך Gate לצמצום רוחב המעגל. התחל עם אותו efficient_su2 אך השתמש בסבכה "לינארית".
qc = efficient_su2(4, entanglement="linear", reps=2)
qc.assign_parameters([0.4] * len(qc.parameters), inplace=True)
observable = SparsePauliOp(["ZZII", "IZZI", "-IIZZ", "XIXI", "ZIZZ", "IXIX"])
print(f"Observable: {observable}")
qc.draw("mpl", scale=0.8)
Observable: SparsePauliOp(['ZZII', 'IZZI', 'IIZZ', 'XIXI', 'ZIZZ', 'IXIX'],
coeffs=[ 1.+0.j, 1.+0.j, -1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])
לאחר מכן צור את תת-המעגלים ותת-ה-observable-ים שתריץ באמצעות הפונקציה partition_problem(). פונקציה זו מקבלת את המעגל, ה-observable, וסכמת חלוקה אופציונלית ומחזירה את המעגלים וה-observable-ים החתוכים בצורת מילון.
החלוקה מוגדרת על ידי מחרוזת תווית בצורת "AABB", כאשר כל תווית במחרוזת זו מתאימה ל-Qubit באינדקס המקביל בארגומנט circuit. Qubit-ים שחולקים תווית חלוקה משותפת מקובצים יחד, וכל Gate לא-מקומי שמשתרע על יותר מחלוקה אחת ייחתך.
הארגומנט observables ל-partition_problem הוא מסוג PauliList. מקדמי וסיבובי אברי ה-observable מתעלמים במהלך פירוק הבעיה וביצוע תת-הניסויים. ניתן להחיל אותם מחדש בעת בניית ערך ההציפייה מחדש.
partitioned_problem = partition_problem(
circuit=qc, partition_labels="AABB", observables=observable.paulis
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
print(f"Subobservables: {subobservables}")
subcircuits["A"].draw("mpl", scale=0.8)
Sampling overhead: 81.0
Subobservables: {'A': PauliList(['II', 'ZI', 'ZZ', 'XI', 'ZZ', 'IX']), 'B': PauliList(['ZZ', 'IZ', 'II', 'XI', 'ZI', 'IX'])}
subcircuits["B"].draw("mpl", scale=0.8)
הצעד הבא הוא להשתמש בתת-המעגלים ובתת-ה-observable-ים כדי לייצר את תת-הניסויים שיבוצעו על QPU באמצעות המתודה generate_cutting_experiments.
כדי לאמוד את ערך ההציפייה של המעגל בגודל המלא, נוצרים תת-ניסויים רבים מהתפלגות הסבירות המשותפת של ה-Gate-ים המפורקים ומבוצעים על QPU אחד או יותר. מספר הדגימות שיילקחו מהתפלגות זו נשלט על ידי הארגומנט num_samples.
בלוק הקוד הבא מייצר את תת-הניסויים ומריץ אותם באמצעות ה-primitive מסוג Sampler על סימולטור מקומי. (כדי להריץ אותם על QPU, שנה את ה-backend למשאב QPU שבחרת.)
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits, observables=subobservables, num_samples=np.inf
)
# Set a backend to use and transpile the subexperiments
backend = FakeManilaV2()
pass_manager = generate_preset_pass_manager(
optimization_level=1, backend=backend
)
isa_subexperiments = {
label: pass_manager.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
# Submit each partition's subexperiments to the Qiskit Runtime Sampler
# primitive, in a single batch so that the jobs will run back-to-back.
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
jobs = {
label: sampler.run(subsystem_subexpts, shots=4096 * 3)
for label, subsystem_subexpts in isa_subexperiments.items()
}
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
לבסוף, ערך ההציפייה של המעגל המלא נבנה מחדש באמצעות המתודה reconstruct_expectation_values.
בלוק הקוד הבא בונה מחדש את התוצאות ומשווה אותן לערך ההציפייה המדויק.
# Get expectation values for each observable term
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
# Reconstruct final expectation value
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)
estimator = EstimatorV2()
exact_expval = (
estimator.run([(qc, observable, [0.4] * len(qc.parameters))])
.result()[0]
.data.evs
)
print(
f"Reconstructed expectation value: {np.real(np.round(reconstructed_expval, 8))}"
)
print(f"Exact expectation value: {np.round(exact_expval, 8)}")
print(
f"Error in estimation: {np.real(np.round(reconstructed_expval-exact_expval, 8))}"
)
print(
f"Relative error in estimation: {np.real(np.round((reconstructed_expval-exact_expval) / exact_expval, 8))}"
)
Reconstructed expectation value: 0.53571896
Exact expectation value: 0.56254612
Error in estimation: -0.02682716
Relative error in estimation: -0.04768882
צעדים הבאים
- קרא את המדריך תחילת העבודה עם חיתוך מעגלים באמצעות חיתוכי Wire.
- קרא את המאמר ב-arXiv על circuit knitting עם תקשורת קלאסית.