דלג לתוכן הראשי
פרויקט זה בשלב בטא. מצאת באג? פתח issue ב-GitHub. יש שאלה או רעיון? שאל ב-Discussions — נשמח לשמוע ממך!
מה איכות התרגום בדף זה?

סימולציה קלאסית של מעגלים עם OBP

במדריך הזה תלמדו איך לסמלץ באופן קלאסי מופעים של QuantumCircuit כדי להעריך ערכי תצפית אך ורק באמצעות OBP.

מכיוון ש-OBP לוקח observable ומבצע לו backpropagation דרך מעגל נתון, ה"סימולציה" של מעגל שקולה לחישוב ערך התצפית של ה-observable היעד ביחס למעגל הזה. כפי שתראו בהמשך, החבילה qiskit-addon-obp אפילו מסוגלת לטפל במודלי רעש פשוטים, כך שאפשר לחשב גם ערכי תצפית רועשים!

בניית מעגל לדוגמה​

לצורך המדריך הזה נשתמש באותו מעגל לדוגמה כמו במדריך קיצוץ איברי Pauli:

# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util

_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_obp": "qiskit-addon-obp", "qiskit_addon_utils": "qiskit-addon-utils", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "rustworkx": "rustworkx"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import rustworkx.generators
from qiskit.synthesis import LieTrotter
from qiskit_addon_utils.problem_generators import (
PauliOrderStrategy,
generate_time_evolution_circuit,
generate_xyz_hamiltonian,
)
from qiskit_addon_utils.slicing import combine_slices, slice_by_gate_types

# we generate a linear chain of 10 qubits
num_qubits = 10
linear_chain = rustworkx.generators.path_graph(num_qubits)

# we use an arbitrary XY model
hamiltonian = generate_xyz_hamiltonian(
linear_chain,
coupling_constants=(0.05, 0.02, 0.0),
ext_magnetic_field=(0.02, 0.08, 0.0),
pauli_order_strategy=PauliOrderStrategy.InteractionThenColor,
)
# we evolve for some time
circuit = generate_time_evolution_circuit(
hamiltonian, synthesis=LieTrotter(reps=3), time=2.0
)
# slice the circuit by gate type
slices = slice_by_gate_types(circuit)

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

כמובן, אפשר היה לבחור במצב האפס (או הוואקום) כמצב ההתחלתי, אבל כדי להראות איך מכניסים מצב התחלתי משלנו, נבחר להלן במצב אחר.

אפשרות אחת היא לצרף את המצב ההתחלתי בתחילת מעגל ההתפתחות בזמן שלמעלה: circuit.compose(initial_state, front=True). אבל מכיוון שכבר חתכנו את ה-circuit שלנו לפרוסות, קל יותר פשוט להכניס את המצב ההתחלתי כפרוסה הראשונה, וכך נעשה להלן.

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

from qiskit.circuit import QuantumCircuit

initial_state = QuantumCircuit(num_qubits)
for i in range(0, num_qubits, 2):
initial_state.x(i)

slices.insert(0, initial_state)
# for visualization purposes only, we recombine the slices with barriers between them and draw the resulting circuit
combine_slices(slices, include_barriers=True).draw("mpl", fold=50, scale=0.6)

Output of the previous code cell

סימולציה של ערך תצפית ללא רעש​

בתור ה-observable היעד נבחר ב-observable ‏ZZ על הקיוביטים המרכזיים:

from qiskit.quantum_info import SparsePauliOp

obs = SparsePauliOp("IIIIZZIIII")

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

from qiskit_addon_obp import backpropagate

vacuum_state_obs, _, metadata = backpropagate(obs, slices)

ביצענו עכשיו backpropagation ל-observable היעד obs שלנו דרך המעגל כולו (כולל ה-initial_state שהצבנו ב-slices[0]), וקיבלנו SparsePauliOp חדש שאת ערך התצפית שלו משיגים על ידי הטלתו על מצב הוואקום (|00...00>).

אפשר לעשות זאת בקלות על ידי סיכום המקדמים של כל איברי ה-Pauli שמוגדרים בבסיס החישובי:

vacuum_state_obs.coeffs[~vacuum_state_obs.paulis.x.any(axis=1)].sum()
np.complex128(-0.8285688012239535+4.9487770271457865e-20j)

כבדיקת שפיות (וכדי להוכיח שזה עובד) אפשר להשוות את התוצאה שלנו ל-Statevector של Qiskit:

from qiskit.quantum_info import Statevector

Statevector(combine_slices(slices)).expectation_value(obs)
np.complex128(-0.8285687255430366+0j)

כמה הערות על ביצועים​

היעילות החישובית של הקריאה ל-backpropagate שלמעלה תלויה מאוד בכמה דברים, ביניהם:

  • המבנה של ה-circuit
  • שיטת חיתוך המעגל לפרוסות
  • ה-observable היעד
  • פרמטרי הקיצוץ

מכיוון שהשיטה backpropagate מפשטת את ה-observable אחרי שכל פרוסה מיושמת, מספר השערים בפרוסה יכול להשפיע דרמטית על העומס החישובי. את אסטרטגיית הפישוט האגרסיבית ביותר של האופרטור אפשר להשיג על ידי חיתוך המעגל לפרוסות של שערים בודדים.

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

סימולציה של ערך תצפית רועש​

החבילה qiskit-addon-obp תומכת גם בטיפול במודלי רעש בצורת PauliLindbladErrors. זה שימושי במיוחד כשאפיינתם את מודל הרעש של שכבות 2 הקיוביטים במעגל שלכם, למשל באמצעות NoiseLearner.

בחלק הזה תראו איך אפשר להשתמש באובייקטי LayerError שמוחזרים מ-NoiseLearner כדי לחשב ערכי תצפית רועשים באמצעות OBP.

קבלת מודל רעש​

בדרך כלל היינו מריצים את NoiseLearner כדי לקבל מודל רעש של המעגל הספציפי שלכם. כדי להימנע ממורכבות (ומאקראיות) במדריך הזה, לא נעשה זאת, ובמקום זאת נקודד להלן מודל רעש כלשהו עבור המעגל שלנו.

עם זאת, אנחנו דואגים שמבנה הנתונים שלנו יתאים לזה של NoiseLearnerResult. בצורתו הנוכחית (שלא עברה transpile), המעגל שלנו מכיל 4 שכבות ייחודיות של שערי 2 קיוביטים:

  • slices[1]: שבה שערי Rxx פועלים על כל הזוגות האי-זוגיים של קיוביטים
  • slices[2]: שבה שערי Rxx פועלים על כל הזוגות הזוגיים של קיוביטים
  • slices[3]: שבה שערי Ryy פועלים על כל הזוגות האי-זוגיים של קיוביטים
  • slices[4]: שבה שערי Ryy פועלים על כל הזוגות הזוגיים של קיוביטים

בתא שלהלן אנחנו בונים ידנית 4 מופעי LayerError, אחד לכל שכבה כזו, עם שיעורי שגיאה אקראיים.

import numpy as np
from qiskit.quantum_info import PauliList
from qiskit_ibm_runtime.utils.noise_learner_result import (
LayerError,
PauliLindbladError,
)

# fmt: off
pauli_errors_even = ['IIIIIIIIIX', 'IIIIIIIIIY', 'IIIIIIIIIZ', 'IIIIIIIIXI', 'IIIIIIIIXX', 'IIIIIIIIXY', 'IIIIIIIIXZ', 'IIIIIIIIYI', 'IIIIIIIIYX', 'IIIIIIIIYY', 'IIIIIIIIYZ', 'IIIIIIIIZI', 'IIIIIIIIZX', 'IIIIIIIIZY', 'IIIIIIIIZZ', 'IIIIIIIXII', 'IIIIIIIXXI', 'IIIIIIIXYI', 'IIIIIIIXZI', 'IIIIIIIYII', 'IIIIIIIYXI', 'IIIIIIIYYI', 'IIIIIIIYZI', 'IIIIIIIZII', 'IIIIIIIZXI', 'IIIIIIIZYI', 'IIIIIIIZZI', 'IIIIIIXIII', 'IIIIIIXXII', 'IIIIIIXYII', 'IIIIIIXZII', 'IIIIIIYIII', 'IIIIIIYXII', 'IIIIIIYYII', 'IIIIIIYZII', 'IIIIIIZIII', 'IIIIIIZXII', 'IIIIIIZYII', 'IIIIIIZZII', 'IIIIIXIIII', 'IIIIIXXIII', 'IIIIIXYIII', 'IIIIIXZIII', 'IIIIIYIIII', 'IIIIIYXIII', 'IIIIIYYIII', 'IIIIIYZIII', 'IIIIIZIIII', 'IIIIIZXIII', 'IIIIIZYIII', 'IIIIIZZIII', 'IIIIXIIIII', 'IIIIXXIIII', 'IIIIXYIIII', 'IIIIXZIIII', 'IIIIYIIIII', 'IIIIYXIIII', 'IIIIYYIIII', 'IIIIYZIIII', 'IIIIZIIIII', 'IIIIZXIIII', 'IIIIZYIIII', 'IIIIZZIIII', 'IIIXIIIIII', 'IIIXXIIIII', 'IIIXYIIIII', 'IIIXZIIIII', 'IIIYIIIIII', 'IIIYXIIIII', 'IIIYYIIIII', 'IIIYZIIIII', 'IIIZIIIIII', 'IIIZXIIIII', 'IIIZYIIIII', 'IIIZZIIIII', 'IIXIIIIIII', 'IIXXIIIIII', 'IIXYIIIIII', 'IIXZIIIIII', 'IIYIIIIIII', 'IIYXIIIIII', 'IIYYIIIIII', 'IIYZIIIIII', 'IIZIIIIIII', 'IIZXIIIIII', 'IIZYIIIIII', 'IIZZIIIIII', 'IXIIIIIIII', 'IXXIIIIIII', 'IXYIIIIIII', 'IXZIIIIIII', 'IYIIIIIIII', 'IYXIIIIIII', 'IYYIIIIIII', 'IYZIIIIIII', 'IZIIIIIIII', 'IZXIIIIIII', 'IZYIIIIIII', 'IZZIIIIIII', 'XIIIIIIIII', 'XXIIIIIIII', 'XYIIIIIIII', 'XZIIIIIIII', 'YIIIIIIIII', 'YXIIIIIIII', 'YYIIIIIIII', 'YZIIIIIIII', 'ZIIIIIIIII', 'ZXIIIIIIII', 'ZYIIIIIIII', 'ZZIIIIIIII']
pauli_errors_odd = ['IIIIIIIIXI', 'IIIIIIIIYI', 'IIIIIIIIZI', 'IIIIIIIXII', 'IIIIIIIXXI', 'IIIIIIIXYI', 'IIIIIIIXZI', 'IIIIIIIYII', 'IIIIIIIYXI', 'IIIIIIIYYI', 'IIIIIIIYZI', 'IIIIIIIZII', 'IIIIIIIZXI', 'IIIIIIIZYI', 'IIIIIIIZZI', 'IIIIIIXIII', 'IIIIIIXXII', 'IIIIIIXYII', 'IIIIIIXZII', 'IIIIIIYIII', 'IIIIIIYXII', 'IIIIIIYYII', 'IIIIIIYZII', 'IIIIIIZIII', 'IIIIIIZXII', 'IIIIIIZYII', 'IIIIIIZZII', 'IIIIIXIIII', 'IIIIIXXIII', 'IIIIIXYIII', 'IIIIIXZIII', 'IIIIIYIIII', 'IIIIIYXIII', 'IIIIIYYIII', 'IIIIIYZIII', 'IIIIIZIIII', 'IIIIIZXIII', 'IIIIIZYIII', 'IIIIIZZIII', 'IIIIXIIIII', 'IIIIXXIIII', 'IIIIXYIIII', 'IIIIXZIIII', 'IIIIYIIIII', 'IIIIYXIIII', 'IIIIYYIIII', 'IIIIYZIIII', 'IIIIZIIIII', 'IIIIZXIIII', 'IIIIZYIIII', 'IIIIZZIIII', 'IIIXIIIIII', 'IIIXXIIIII', 'IIIXYIIIII', 'IIIXZIIIII', 'IIIYIIIIII', 'IIIYXIIIII', 'IIIYYIIIII', 'IIIYZIIIII', 'IIIZIIIIII', 'IIIZXIIIII', 'IIIZYIIIII', 'IIIZZIIIII', 'IIXIIIIIII', 'IIXXIIIIII', 'IIXYIIIIII', 'IIXZIIIIII', 'IIYIIIIIII', 'IIYXIIIIII', 'IIYYIIIIII', 'IIYZIIIIII', 'IIZIIIIIII', 'IIZXIIIIII', 'IIZYIIIIII', 'IIZZIIIIII', 'IXIIIIIIII', 'IXXIIIIIII', 'IXYIIIIIII', 'IXZIIIIIII', 'IYIIIIIIII', 'IYXIIIIIII', 'IYYIIIIIII', 'IYZIIIIIII', 'IZIIIIIIII', 'IZXIIIIIII', 'IZYIIIIIII', 'IZZIIIIIII']
# fmt: on

np.random.seed(42)

layer_error_odd_xx = LayerError(
circuit=slices[1],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_odd),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_odd)),
),
)

layer_error_even_xx = LayerError(
circuit=slices[2],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_even),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_even)),
),
)

layer_error_odd_yy = LayerError(
circuit=slices[3],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_odd),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_odd)),
),
)

layer_error_even_yy = LayerError(
circuit=slices[4],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_even),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_even)),
),
)

אילו הייתם משתמשים ב-NoiseLearner כדי לזהות את שכבות שערי 2 הקיוביטים הייחודיות של המעגל שלכם ולאפיין את הרעש שלהן, הייתם מקבלים אובייקט NoiseLearnerResult. התוצאה הזו הייתה מכילה רשימה של אובייקטי LayerError, בדיוק כמו אלה שבנינו ידנית למעלה.

לכל שכבה ייחודית של 2 קיוביטים, ה-LayerError מכיל:

  • את ה-QuantumCircuit שמייצג את שכבת שערי 2 הקיוביטים הזו
  • את אינדקסי הקיוביטים שהמעגל הזה פועל עליהם
  • את ה-PauliLindbladError שמייצג את מודל הרעש המאופיין של השכבה הזו

ה-PauliLindbladError יכיל שני אובייקטים:

  • רשימת שגיאות ה-Pauli שאופיינו
  • שיעורי השגיאה שמתאימים לכל אחת משגיאות ה-Pauli האלה

בדרך כלל רשימת שגיאות ה-Pauli תהיה דלילה. ליתר דיוק, היא תכיל את שגיאות ה-Pauli של קיוביט בודד על כל הקיוביטים שפועלים עליהם שערים, וגם את שגיאות ה-Pauli של שני קיוביטים על כל הקיוביטים המחוברים.

הכנסת השכבות הרועשות למעגל שלנו​

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

בדרך כלל, כשמשתמשים ב-LayerError, צריך להבין מהי שכבת שערי 2 הקיוביטים הייחודית ואיפה היא מופיעה בתוך המעגל. אחר כך צריך להתאים את ה-circuit ו/או את ה-slices כדי להכניס את ה-LayerError בהתאם. איך לעשות זאת במקרה הכללי חורג מהיקף המדריך הזה. TODO: link to external documentation, once it exists! כאן החיים שלנו קלים יותר כי אנחנו יודעים לאיזו פרוסה מתאים כל LayerError. לכן, כל מה שנשאר הוא להכניס פרוסות חדשות שמייצגות את הרעש.

שימו לב שאנחנו חייבים לעטוף כל PauliLindbladError מ-LayerError.error ב-PauliLindbladErrorInstruction כדי שיהיה הוראה תקינה של QuantumCircuit.

from qiskit_addon_obp.utils.noise import PauliLindbladErrorInstruction

noisy_slices = []
for slice_ in slices:
if slice_ == layer_error_even_xx.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_even_xx.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_odd_xx.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_odd_xx.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_even_yy.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_even_yy.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_odd_yy.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_odd_yy.error
),
slice_.qubits,
)
]
)
)
noisy_slices.append(slice_)

אפשר לבדוק את העבודה שלנו ולצייר את המעגל להלן:

combine_slices(noisy_slices, include_barriers=True).draw(
"mpl", fold=100, scale=0.8
)

Output of the previous code cell

סימולציה של ערך תצפית רועש​

בשלב הזה, הסימולציה הקלאסית של ערך התצפית עובדת בדיוק כמו קודם, רק

vacuum_state_noisy_obs, _, metadata = backpropagate(obs, noisy_slices)
vacuum_state_noisy_obs.coeffs[
~vacuum_state_noisy_obs.paulis.x.any(axis=1)
].sum()
np.complex128(-0.7230801696448901+7.082755280463563e-19j)

אנחנו מציינים שוב שיש לקחת בחשבון כמה שיקולי ביצועים. אנא חזרו לחלק המתאים שלמעלה.