איך מהדקים חסמים עם speed limit
מידע במעגל של שערים דו-קיוביטיים לא יכול להתפשט מהר כרצוננו. שגיאה שמתרחשת כמה שכבות לפני הסוף, על קיוביט רחוק מהתומך של ה-observable, אין לה דרך להגיע אליו. tighten_with_speed_limit הופכת את הטיעון הגיאומטרי הזה (אותה אינטואיציה שמאחורי חסמי Lieb-Robinson) לחסמים קדימה הדוקים יותר.
השלב הזה זול, לא צריך שום דבר מעבר למה שחסמי הקדימה כבר דרשו, ויכול רק להדק את החסמים. אין פשרה בדיוק שצריך לשקול.
השאלה ששווה לענות עליה קודם היא "מתי זה עוזר?", כי זה לא עוזר לעיתים קרובות על קבוצת חסמי קדימה שכבר התכנסה במלואה.
הגדרה
משתמשים באותו מעגל Ising בקירוב Trotter כמו במדריך ההתחלה המהירה, עם ה-observable על קיוביט בודד באמצע השרשרת. הגיאומטריה החד-ממדית מקלה על ההסבר: השפעה צריכה לעבור לאורך השרשרת קשר אחד בכל שכבה.
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_slc": "qiskit-addon-slc", "samplomatic": "samplomatic"}
_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 numpy as np
from qiskit import QuantumCircuit
from qiskit.quantum_info import Pauli
from qiskit_addon_slc.bounds import (
compute_forward_bounds,
tighten_with_speed_limit,
)
from qiskit_addon_slc.utils import generate_noise_model_paulis
from samplomatic.transpiler import generate_boxing_pass_manager
from samplomatic.utils import find_unique_box_instructions
def trotter_ising_circuit(num_qubits, num_steps, rx_angle, rzz_angle):
"""Trotterized transverse-field Ising evolution on a 1D chain."""
circuit = QuantumCircuit(num_qubits)
for _ in range(num_steps):
circuit.rx(rx_angle, range(num_qubits))
circuit.barrier()
for start in (0, 1): # even then odd bonds
for i in range(start, num_qubits - 1, 2):
circuit.rzz(rzz_angle, i, i + 1)
circuit.barrier()
return circuit
num_qubits = 10
circuit = trotter_ising_circuit(
num_qubits, num_steps=4, rx_angle=np.pi / 16, rzz_angle=-np.pi / 3
)
# A single-qubit observable in the middle of the chain.
observable = Pauli("I" * num_qubits).compose("Z", [num_qubits // 2])
boxing_pass = generate_boxing_pass_manager(
inject_noise_targets="all",
inject_noise_strategy="individual_modification",
inject_noise_site="after",
twirling_strategy="active",
remove_barriers="never",
)
boxed_circuit = boxing_pass.run(circuit)
noise_model_paulis = generate_noise_model_paulis(
find_unique_box_instructions(boxed_circuit)
)
def compare(label, bounds):
"""Tightens a set of bounds and prints what changed."""
tightened = tighten_with_speed_limit(
bounds, boxed_circuit, noise_model_paulis, observable
)
before = np.concatenate([bound.rates for bound in bounds.values()])
after = np.concatenate([bound.rates for bound in tightened.values()])
print(
f"{label:30} mean {before.mean():.4f} -> {after.mean():.4f} "
f"tightened {np.count_nonzero(after < before - 1e-12):4} of {len(before)} "
f"loosened {np.count_nonzero(after > before + 1e-12)}"
)
print(
f"observable: Z on qubit {num_qubits // 2}, circuit depth {circuit.depth()}"
)
observable: Z on qubit 5, circuit depth 12
מתי זה עוזר, ומתי לא
tighten_with_speed_limit לוקחת את החסמים שכבר יש לך ומחזירה עותק מהודק; החסמים המקוריים שלך אף פעם לא משתנים. כמה היא מרוויחה תלוי כולו בכמה רופף היה הקלט.
# 1. Fully computed forward bounds.
full_bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable
)
compare("fully computed", full_bounds)
# 2. Bounds where the eigenvalue step was skipped, leaving the looser triangle inequality.
loose_bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable, eigval_max_qubits=0
)
compare("triangle inequality only", loose_bounds)
# 3. Bounds computed only for the first few boxes, the rest left at the trivial bound of 2.0.
shallow_bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable, max_num_boxes=2
)
compare("max_num_boxes=2", shallow_bounds)
fully computed mean 0.3639 -> 0.3639 tightened 0 of 792 loosened 0
triangle inequality only mean 0.5081 -> 0.4842 tightened 105 of 792 loosened 0
max_num_boxes=2 mean 1.5707 -> 0.5284 tightened 450 of 792 loosened 0
השורה הראשונה היא זו שכדאי לשים לב אליה. על חסמי קדימה שחושבו במלואם, ה-speed limit לא משנה כלום. זו לא כשלון. זה אומר שהחסמים כבר היו הדוקים לפחות כמו שהטיעון הגיאומטרי יכול להוכיח, כי התפתחות האופרטור גילתה את חרוט האור בעצמה.
הרווחים מופיעים איפה שהחסמים לא חושבו במלואם:
-
eigval_max_qubits=0כופה את הגיבוי של אי-שוויון המשולש, ולכן החסמים כבר לא משקפים את הנורמה הספקטרלית האמיתית של הקומוטטור. ה-speed limit משחזר חלק מההפסד הזה. -
max_num_boxes=2משאיר את רוב השכבות בחסם הטריוויאלי 2.0 (חסם שלא מקודד שום מידע). כאן ה-speed limit עוזר, ומקטין את הממוצע בערך פי שלושה.
בכל המקרים העמודה "התרופף" היא אפס. אף איבר אף פעם לא מחמיר, וזו הערובה שהופכת את השלב הזה לבטוח להפעלה ללא תנאי.
מה זה קונה בתקורת הדגימה
חסמים הדוקים יותר אומרים שיש פחות איברי שגיאה שצריך לקזז עבור סבילות הטיה נתונה, וזה המספר שחשוב בחומרה.
from qiskit.quantum_info import PauliLindbladMap, QubitSparsePauliList
from qiskit_addon_slc.bounds import compute_local_scales
# Stand-in for rates that a noise-learning experiment would measure on hardware.
rng = np.random.default_rng(42)
noise_rates = {
layer: PauliLindbladMap.from_components(
rng.random(len(terms)) * 5e-3,
QubitSparsePauliList.from_sparse_list(
terms.to_sparse_list(), terms.num_qubits
),
)
for layer, terms in noise_model_paulis.items()
}
tightened_shallow = tighten_with_speed_limit(
shallow_bounds, boxed_circuit, noise_model_paulis, observable
)
print(
f"{'bounds':30} {'overhead':>9} {'mitigated':>10} {'residual bias':>14}"
)
for label, bounds in (
("max_num_boxes=2", shallow_bounds),
("max_num_boxes=2 + speed limit", tightened_shallow),
("fully computed", full_bounds),
):
scales, cost, bias = compute_local_scales(
boxed_circuit, bounds, noise_rates, bias_tolerance=0.05
)
mitigated = sum(int(np.count_nonzero(scale)) for scale in scales.values())
total = sum(len(scale) for scale in scales.values())
print(
f"{label:30} {cost:9.4f} {f'{mitigated}/{total}':>10} {bias:>14.4f}"
)
bounds overhead mitigated residual bias
max_num_boxes=2 420.2823 617/792 0.0490
max_num_boxes=2 + speed limit 7.8511 226/792 0.0485
fully computed 5.4031 188/792 0.0494
מתי להשתמש בזה
מפעילים את זה על חסמי קדימה בכל פעם שהחסמים האלה לא התכנסו במלואם (בעצם, בכל פעם שוויתרת על דיוק לטובת מהירות באמצעות אחת מהאפשרויות במדריך התצורה). זה זול בהשוואה לחישוב החסמים, לא יכול להחמיר שום דבר, ומשחזר הרבה ממה שמגבלת max_num_boxes או eigval_max_qubits ויתרה עליו.
על חסמים שחושבו במלואם זו פעולה ריקה, כך שאין נזק בהפעלה שלה ללא תנאי.
שתי מגבלות:
-
זה חל על חסמי קדימה בלבד. חסמי אחורה עוסקים בהשפעת השגיאה על המצב ההתחלתי ולא על ה-observable, ולכן הטיעון לא עובר אליהם.
-
ה-observable חייב להיות איבר פאולי יחיד. עבור observable עם כמה איברים, קוראים לזה פעם אחת לכל פאולי ומשלבים את התוצאות בעצמך. אם ה-observable אינו איבר פאולי יחיד, מועלית
NotImplementedError.
בזרימת העבודה הרגילה, זה שייך בין חישוב חסמי הקדימה למיזוגם עם חסמי האחורה, כפי שמראה מדריך ההתחלה המהירה.