איך לכוונן את אפשרויות התצורה
חישוב החסמים מדויק בעיקרון אבל מקורב בפועל: התפתחות של איבר שגיאה מסוג Pauli דרך מעגל מפיקה אופרטור עם מספר איברים שגדל אקספוננציאלית, ומציאת הנורמה הספקטרלית של הקומוטטור המתקבל היא בעצמה פעולה יקרה. האפשרויות שלהלן שולטות בעלות הזו. הן מתחלקות לשלוש קבוצות:
-
אפשרויות דיוק משנות את הערך המספרי של החסמים, ומחליפות הדוק בעלות.
-
היקף קובע אילו חסמים מחושבים.
-
אפשרויות ביצוע משנות איך החישוב רץ (ולא מה הוא מחשב).
| אפשרות | ברירת מחדל | שולטת ב | השפעה על התוצאה |
|---|---|---|---|
| דיוק | |||
evolution_max_terms | 1_000_000 | כמה איברי אופרטור שורדים את ההתפתחות | נמוך יותר: זול יותר, חסמים רופפים יותר |
atol_simplify | 1e-8 | אילו איברי קומוטטור נזרקים כזניחים | גבוה יותר: זול יותר, חסמים רופפים יותר |
eigval_max_qubits | 14 | עד איזה רוחב קומוטטור מחשבים את הנורמה הספקטרלית שלו | נמוך יותר: זול יותר, חסמים רופפים יותר |
atol_eigenvalue | 1e-10 | מתי פותר הערכים העצמיים האיטרטיבי נחשב כמי שהתכנס | גבוה יותר: זול יותר, חסמים פחות מדויקים |
| היקף | |||
max_num_boxes | None | לכמה תיבות מחושבים חסמים | תיבות מעבר לגבול שומרות על החסם הטריוויאלי 2.0 |
| ביצוע | |||
timeout | None | תקציב זמן אמיתי (wall-clock) לכל החישוב | כל עבודה שנותרה שומרת על החסם הטריוויאלי 2.0 |
num_processes | 1 | כמה תהליכי עבודה מחשבים חסמים במקביל | אין לה השפעה כשלעצמה |
חסם שימושי רק אם הוא תקף: הוא לא יכול להמעיט בהשפעה של איבר שגיאה על האובזרבבל. הרפיית כל אחת מהאפשרויות האלה, מלבד atol_eigenvalue, יכולה רק להגדיל את החסמים (להיות שמרנית יותר), אף פעם לא להקטין אותם. לכן המחיר של הגדרה רפויה הוא תקורת דגימה גדולה יותר, ולא ערך תוחלת שגוי - atol_eigenvalue היא היוצאת מן הכלל.
הגדרת המעגל
הניסויים הבאים משתמשים באותו מעגל Ising מסוג Trotter ובאותו אובזרבבל כמו במדריך ההתחלה המהירה. בגודל הזה לכל אפשרות יש השפעה נראית לעין, וכל חישוב עדיין לוקח פחות משנייה.
# 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.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
)
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)
)
print(f"noisy layers: {len(noise_model_paulis)}")
print(
f"Pauli error terms: {sum(len(p) for p in noise_model_paulis.values())}"
)
noisy layers: 2
Pauli error terms: 198
פונקציית עזר קטנה מסכמת כל קבוצת חסמים בשורה אחת, כך שהשפעת האפשרות מופיעה כמה מספרים בודדים ולא כקיר של מערכים.
כל חסם נמצא בין 0 לבין החסם הטריוויאלי 2.0. הסיכום מדווח על החסם הממוצע ועל סטיית התקן שלו, ואחר כך על האופן שבו האיברים מתחלקים לקבוצות האלה:
- איברים שהחסם שלהם הוא בדיוק 0 כי הם מתחלפים עם האובזרבבל המתפתח
- איברים עם חסם מחושב בין 0 ל-2.0
- איברים שנשארו עם 2.0 הטריוויאלי
שימו לב שממוצע נמוך יותר הוא הדוק יותר, והדוק יותר אומר תקורת דגימה קטנה יותר בהמשך.
סריקה על אפשרות אחת מסמנת את ברירת המחדל של האפשרות ב-*:
import inspect
import time
from qiskit_addon_slc.bounds import compute_forward_bounds
def summarize(rates, label, elapsed):
"""Prints a one-line summary of a set of bounds."""
# The bounds are floating-point results, so a term counts as bounded by 0, or as left at the
# trivial 2.0, when it is within rounding error of that value.
exact = np.isclose(rates, 0.0, rtol=0.0, atol=1e-12)
trivial = np.isclose(rates, 2.0, rtol=0.0, atol=1e-12)
computed = ~exact & ~trivial
print(
f"{label:28} mean {rates.mean():.4f} ± {rates.std():.3f} │ "
f"at 0: {exact.mean():4.0%} computed: {computed.mean():4.0%} "
f"at 2.0: {trivial.mean():4.0%} │ "
f"{elapsed:4.2f}s"
)
def forward(label, **kwargs):
"""Computes the forward bounds, summarizes them and returns all their rates."""
start = time.perf_counter()
bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable, **kwargs
)
elapsed = time.perf_counter() - start
rates = np.concatenate([bound.rates for bound in bounds.values()])
summarize(rates, label, elapsed)
return rates
def sweep(option, values):
"""Computes the forward bounds for each value of one option, marking the default with `*`."""
default = (
inspect.signature(compute_forward_bounds).parameters[option].default
)
for value in values:
marker = "*" if value == default else " "
forward(f"{marker} {option}={value:g}", **{option: value})
baseline = forward(" defaults")
defaults mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.29s
דיוק
evolution_max_terms
התפתחות של איבר שגיאה מסוג Pauli דרך שערים שאינם Clifford הופכת אותו לסכום של איברי Pauli, והסכום הזה גדל עם עומק המעגל. evolution_max_terms מגביל כמה מהאיברים האלה נשמרים. נורמת ה-1 של כל מה שנזרק מתווספת לחסם, כך שדבר לא הולך לאיבוד בשקט.
הסריקה רצה מהדוק יותר מברירת המחדל ועד חיתוך קיצוני. הורדת התקרה הופכת את החישוב לזול יותר ואת החסמים לרפויים יותר.
sweep("evolution_max_terms", (10_000_000, 1_000_000, 1000, 100, 10, 2))
evolution_max_terms=1e+07 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.38s
* evolution_max_terms=1e+06 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.31s
evolution_max_terms=1000 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.30s
evolution_max_terms=100 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.29s
evolution_max_terms=10 mean 0.3927 ± 0.711 │ at 0: 59% computed: 31% at 2.0: 9% │ 0.28s
evolution_max_terms=2 mean 0.5245 ± 0.792 │ at 0: 59% computed: 23% at 2.0: 18% │ 0.21s
העלאת התקרה מעבר לברירת המחדל לא משנה כאן דבר. במעגל בגודל הזה ברירת המחדל כבר שומרת על כל האיברים, ולכן החסמים הדוקים ככל שהאפשרות הזו מסוגלת להשיג. זה המצב האופייני. החמרה של אפשרות דיוק משתלמת רק כשברירת המחדל שלה באמת מגבילה, והדרך לגלות זאת היא הניסוי שלמעלה. הריצו אותו שוב עם ערך הדוק יותר ובדקו אם החסמים זזים.
אזהרה: שגרת ההתפשטות שמתחת לפני השטח מתייחסת ל-evolution_max_terms כקיבולת להקצאה מראש, ולא כגבול עליון שבודקים מולו. העברת ערך שנחשב לכאורה בלתי מוגבל, כמו numpy.iinfo(numpy.uint).max, גורמת למקצה הזיכרון לבקש טרה-בייטים של זיכרון ומפילה את התהליך. הישארו עם מספר מציאותי. ברירת המחדל של מיליון היא כבר נדיבה.
atol_simplify
אחרי שהקומוטטור נוצר, איברים עם מקדם הנמוך מ-atol_simplify נזרקים. כמו בתקרת ההתפתחות, נורמת ה-1 שאבדה בדרך הזו מתווספת לחסם.
sweep("atol_simplify", (0.0, 1e-8, 1e-4, 1e-2, 1e-1))
atol_simplify=0 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.32s
* atol_simplify=1e-08 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.31s
atol_simplify=0.0001 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.33s
atol_simplify=0.01 mean 0.3656 ± 0.685 │ at 0: 59% computed: 35% at 2.0: 6% │ 0.31s
atol_simplify=0.1 mean 0.3837 ± 0.697 │ at 0: 59% computed: 31% at 2.0: 10% │ 0.31s
eigval_max_qubits
החסם ההדוק ביותר שזמין הוא הנורמה הספקטרלית של הקומוטטור, והיא דורשת חישוב ערכים עצמיים. החישוב הזה נעשה יקר ככל שהקומוטטור מתרחב, ולכן eigval_max_qubits קובע את הרוחב שמעבר לו מוותרים על הניסיון לטובת אי-שוויון המשולש. החסם הזה, על פני המקדמים של הקומוטטור, תקף אבל רפוי יותר.
הגדרתו לאפס כופה את חלופת הגיבוי בכל מקום, וכך מראה את התועלת של פותר הערכים העצמיים במספר אחד.
sweep("eigval_max_qubits", (20, 14, 8, 4, 2, 0))
eigval_max_qubits=20 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.31s
* eigval_max_qubits=14 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.30s
eigval_max_qubits=8 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.32s
eigval_max_qubits=4 mean 0.3742 ± 0.694 │ at 0: 59% computed: 35% at 2.0: 6% │ 0.30s
eigval_max_qubits=2 mean 0.4983 ± 0.809 │ at 0: 59% computed: 23% at 2.0: 18% │ 0.27s
eigval_max_qubits=0 mean 0.5081 ± 0.810 │ at 0: 59% computed: 21% at 2.0: 19% │ 0.27s
גם כשפותר הערכים העצמיים כבוי, הממוצע נשאר נמוך בהרבה מ-2.0 הטריוויאלי. חלקם של האיברים שהחסם שלהם הוא 0 לא משתנה: איבר שמתחלף בדיוק עם האובזרבבל המתפתח הוא בעל קומוטטור אפס, שמזוהה עוד לפני שמתחשבים בחישוב ערכים עצמיים כלשהו. מבין האחרים, חסם אי-שוויון המשולש מגיע ל-2.0 אצל חלקם ונחתך שם, אבל אצל רבים הוא עדיין קטן יותר, ולכן הם שומרים על חסם מחושב מתחת ל-2.0.
atol_eigenvalue
כשהנורמה הספקטרלית כן מחושבת, atol_eigenvalue הוא סבולת ההתכנסות של פותר הערכים העצמיים האיטרטיבי. ובכל זאת, רוב הקומוטטורים אף פעם לא מגיעים לפותר הזה:
-
קומוטטור הפועל על ארבעה Qubit לכל היותר עובר אלכסון מדויק, ישירות.
-
קומוטטור רחב יותר מצטמצם קודם למספר הקטן ביותר של Qubit שאיברי ה-Pauli שלו צריכים, ואם הצורה המצומצמת פועלת על שמונה Qubit לכל היותר, הוא עובר אלכסון מדויק.
רק קומוטטור שעדיין צריך יותר משמונה Qubit אחרי הצמצום הזה עובר לפותר האיטרטיבי, שהוא המקום היחיד שבו הסבולת הזו חלה. מעגלים בגודל שבו משתמשים במדריך הזה אף פעם לא מגיעים לשם, ולכן סריקה על האפשרות הזו תראה חסמים זהים לכל ערך. היא משנה לחרוטי אור רחבים בהתקנים גדולים.
בניגוד לאפשרויות הדיוק האחרות, הרפיית atol_eigenvalue אינה בטוחה בכיוון אחד. פותר ערכים עצמיים איטרטיבי מתקרב לערך העצמי הקיצוני מבפנים הספקטרום, ולכן הערכה שלא התכנסה במלואה ממעיטה בנורמה הספקטרלית, ובחסם. ברירת המחדל הדוקה בכוונה מסיבה זו. אם הפותר לא מצליח להתכנס, חלופת הגיבוי של אי-שוויון המשולש נכנסת לפעולה, והיא שוב תקפה.
היקף
max_num_boxes
לפעמים הגבול השימושי הוא לא דיוק אלא עומק. max_num_boxes מפסיק לשגר חישובי חסמים מעבר למספר נתון של תיבות, וכל שכבה מעבר אליו מקבלת במקום זה את החסם הטריוויאלי 2.0.
הגבול סופר את הוראות BoxOp הרועשות (אלה שנושאות הערת InjectNoise) ולא, למשל, עומק במספר שערים. כך הוא נשאר קשור למבנה המעגל שהמשתמש רואה, ונמנע מצימוד להפניות ההערות שנוצרות אוטומטית, שעשויות להשתנות בין ריצות טרנספילציה.
זו הבחירה הטבעית למעגל ששני חציו מטופלים בחסמי קדימה ואחורה בהתאמה: מחשבים כל אחד רק עד העומק שבו ישתמשו בו.
from qiskit.circuit import BoxOp
from samplomatic import InjectNoise
# The limit counts the noisy boxes encountered while iterating the circuit, not the unique ones:
# a repeated layer is one noise model but several boxes. Boxes without an `InjectNoise`
# annotation carry no error terms and do not count.
num_boxes = sum(
1
for inst in boxed_circuit.data
if isinstance(inst.operation, BoxOp)
and any(
isinstance(annotation, InjectNoise)
for annotation in inst.operation.annotations
)
)
print(
f"the circuit has {num_boxes} noisy boxes drawn from "
f"{len(noise_model_paulis)} distinct noise models\n"
)
for limit in (None, 6, 4, 2, 1):
marker = "*" if limit is None else " "
forward(f"{marker} max_num_boxes={limit}", max_num_boxes=limit)
the circuit has 9 noisy boxes drawn from 2 distinct noise models
* max_num_boxes=None mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.33s
max_num_boxes=6 mean 0.7486 ± 0.935 │ at 0: 53% computed: 18% at 2.0: 30% │ 0.18s
max_num_boxes=4 mean 1.1506 ± 0.980 │ at 0: 40% computed: 5% at 2.0: 55% │ 0.11s
max_num_boxes=2 mean 1.5707 ± 0.821 │ at 0: 21% computed: 0% at 2.0: 79% │ 0.05s
max_num_boxes=1 mean 1.8157 ± 0.579 │ at 0: 9% computed: 0% at 2.0: 91% │ 0.03s
ביצוע
timeout
timeout הוא תקציב זמן אמיתי (wall-clock) בשניות. כשהוא פג, החסמים שכבר חושבו נשמרים, וכל מה שנשאר פתוח שומר על החסם הטריוויאלי 2.0, כך שהתוצאה תמיד שמישה, רק רפויה יותר ככל שמפסיקים מוקדם יותר.
זה לא דד-ליין קשיח: משימות שכבר רצות מורשות לסיים, ולכן הקריאה יכולה לחרוג מהתקציב בזמן של משימה אחת.
num_processes
החסמים של איברי שגיאה שונים הם בלתי תלויים, ולכן num_processes מפזר אותם על תהליכי עבודה. כשלעצמו, הדבר משפיע רק על משך החישוב, אף פעם לא על התוצאה.
הערה: שילוב של timeout עם num_processes גורם לתוצאה להיות תלויה בתזמון: אילו משימות מסתיימות לפני שהתקציב פג אינו דטרמיניסטי. החסמים נשארים תקפים, אבל הם כבר לא ניתנים לשחזור מריצה לריצה.
מאיפה מתחילים
ברירות המחדל נבחרו כדי לתת חסמים הדוקים במעגלים בגודל שבו זה משתלם. כשחישוב איטי מדי, נסו להפעיל את האפשרויות בערך בסדר הזה:
-
num_processes: האצה בחינם, בלי מחיר בדיוק. -
max_num_boxes: אם אתם יודעים מראש עד איזה עומק במעגל מעניין אתכם. -
eigval_max_qubits: מדלג על השלב היקר ביותר עבור הקומוטטורים הרחבים ביותר, שהם בדרך כלל מיעוט קטן מהאיברים. -
evolution_max_termsו-atol_simplify: אלה משפיעים על הדיוק לכל אורכו, ולכן כווננו אותם אחרונים, ובדקו בסריקה כמו אלה שלמעלה עד כמה החסמים זזים. -
timeout: עצירה קשיחה ולא כלי כוונון, למקרה שחישוב חייב להסתיים עד דד-ליין, בכל איכות שהיא.
ככל שהחסמים רפויים יותר, תקורת הדגימה ש-compute_local_scales ידווח עליה לסבולת הטיה נתונה גדולה יותר. התקורה הזו היא הגודל ששווה לעקוב אחריו בזמן הכוונון: היא מראה מה ההקירובים עולים בפועל.