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

איך לכוונן את אפשרויות התצורה

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

  • אפשרויות דיוק משנות את הערך המספרי של החסמים, ומחליפות הדוק בעלות.

  • היקף קובע אילו חסמים מחושבים.

  • אפשרויות ביצוע משנות איך החישוב רץ (ולא מה הוא מחשב).

אפשרותברירת מחדלשולטת בהשפעה על התוצאה
דיוק
evolution_max_terms1_000_000כמה איברי אופרטור שורדים את ההתפתחותנמוך יותר: זול יותר, חסמים רופפים יותר
atol_simplify1e-8אילו איברי קומוטטור נזרקים כזניחיםגבוה יותר: זול יותר, חסמים רופפים יותר
eigval_max_qubits14עד איזה רוחב קומוטטור מחשבים את הנורמה הספקטרלית שלונמוך יותר: זול יותר, חסמים רופפים יותר
atol_eigenvalue1e-10מתי פותר הערכים העצמיים האיטרטיבי נחשב כמי שהתכנסגבוה יותר: זול יותר, חסמים פחות מדויקים
היקף
max_num_boxesNoneלכמה תיבות מחושבים חסמיםתיבות מעבר לגבול שומרות על החסם הטריוויאלי 2.0
ביצוע
timeoutNoneתקציב זמן אמיתי (wall-clock) לכל החישובכל עבודה שנותרה שומרת על החסם הטריוויאלי 2.0
num_processes1כמה תהליכי עבודה מחשבים חסמים במקבילאין לה השפעה כשלעצמה

חסם שימושי רק אם הוא תקף: הוא לא יכול להמעיט בהשפעה של איבר שגיאה על האובזרבבל. הרפיית כל אחת מהאפשרויות האלה, מלבד 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 קובע את הרוחב שמעבר לו מוותרים על הניסיון לטובת אי-שוויון המשולש. החסם הזה, 2∑i∣ci∣2 \sum_i |c_i| על פני המקדמים cic_i של הקומוטטור, תקף אבל רפוי יותר.

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

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 גורם לתוצאה להיות תלויה בתזמון: אילו משימות מסתיימות לפני שהתקציב פג אינו דטרמיניסטי. החסמים נשארים תקפים, אבל הם כבר לא ניתנים לשחזור מריצה לריצה.

מאיפה מתחילים​

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

  1. num_processes: האצה בחינם, בלי מחיר בדיוק.

  2. max_num_boxes: אם אתם יודעים מראש עד איזה עומק במעגל מעניין אתכם.

  3. eigval_max_qubits: מדלג על השלב היקר ביותר עבור הקומוטטורים הרחבים ביותר, שהם בדרך כלל מיעוט קטן מהאיברים.

  4. evolution_max_terms ו-atol_simplify: אלה משפיעים על הדיוק לכל אורכו, ולכן כווננו אותם אחרונים, ובדקו בסריקה כמו אלה שלמעלה עד כמה החסמים זזים.

  5. timeout: עצירה קשיחה ולא כלי כוונון, למקרה שחישוב חייב להסתיים עד דד-ליין, בכל איכות שהיא.

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