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

שיפור ערכי תוחלת עם ספיגת רעש מועברת (PNA)

הערכת שימוש: 10 דקות על מעבד Heron (הערה: זו הערכה בלבד. זמן הריצה שלכם עשוי להשתנות.)

תוצרי למידה​

  • מהי ספיגת רעש מועברת (PNA), וכיצד היא מפחיתה רעש שערי שני-קיוביטים על ידי ספיגת ערוצי רעש הפוכים שנלמדו לתוך התצפית (observable) הנמדדת

  • כיצד להשתמש ב-samplomatic כדי לתחום (box) ולתייג שכבות מעגל לצורך twirling, שינויי בסיס, והזרקת רעש

  • כיצד ללמוד רעש שכבה עם [NoiseLearnerV3](/guides/directed-execution-model#noiselearnerv3) ולהעביר אותו לתוך תצפית ממתנת-רעש עם qiskit-addon-pna

  • כיצד לדגום מעגלים אקראיים באמצעות המחלקות QuantumProgram ו-Executor ב-qiskit-ibm-runtime, ולשלב PNA עם TREX ובחירה-לאחר-מכן

דרישות מקדימות​

רקע​

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

מהי ספיגת רעש מועברת (PNA)?​

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

אנחנו יכולים להשתמש במיתון שגיאות קוונטי כדי לחלץ ערכי תוחלת שימושיים מחומרה קוונטית רועשת ללא צורך בסובלנות תקלות מלאה. PNA מתמקד באופן ספציפי בספיגת אפקטי רעש לתוך התצפית עצמה במקום לשנות את פעולות המעגל.

כל שער רועש במעגל קוונטי יכול להיות מודל כשער אידיאלי ואחריו ערוץ רעש. PNA לומד או מאפיין ערוצי רעש אלה ומגדיר את ההופכיים שלהם. במקום להכניס את הפעולות ההפוכות להרצת החומרה (שלרוב אינה אפשרית), PNA מעביר את ערוצי הרעש ההפוכים קדימה דרך המעגל ומיישם אותם על התצפית. תהליך זה הופך את התצפית OO לאופרטור חדש O~\tilde{O}, כך שמדידת O~\tilde{O} במעגל הרועש מניבה את אותו ערך תוחלת כמו מדידת OO במעגל אידיאלי, נטול-רעש.

ניתן לסכם זאת כך:

  1. מדלים כל שער רועש uiu_i כפעולה אידיאלית ואחריה ערוץ רעש Λi\Lambda_i.
  2. לומדים או מעריכים כל Λi\Lambda_i באמצעות כלי אפיון רעש.
  3. מגדירים ומעבירים את מפות הרעש ההפוכות Λi−1\Lambda_i^{-1} קדימה דרך המעגל באמצעות טכניקות העברת פאולי (Pauli transfer).
  4. סופגים את ההפוכים הללו לתוך התצפית, מה שמניב אופרטור ממותן-רעש O~\tilde{O}.

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

על ידי הזזת המיתון לתוך התצפית, PNA נמנע מתקורת הדגימה הגדולה שהייתה נובעת אחרת מהכנסת פעולות תיקון פיזיות למעגל. במקום זאת, המעגל הרועש המקורי מורץ, בעוד שהתצפית מומרת לאופרטור חדש O~\tilde{O} שערך התוחלת שלו מבטל את השפעות הרעש.

זרימת העבודה של PNA​

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

Noisy experiment

אם נלמד את מודל הרעש, נוכל ליישם את ההופכי שלו ולבטל את הרעש.

Noise-mitigated experiment

במקום ליישם את ערוץ הרעש ההפוך על ידי דגימה שלו על ה-QPU כפי שנעשה בביטול שגיאה הסתברותי (PEC), אנחנו מיישמים אותו באופן קלאסי על התצפית הנמדדת באמצעות העברת פאולי. התצפית המתקבלת ממתנת ביעילות את רעש השער הנלמד כאשר היא נמדדת.

PNA overview

מיתון שגיאות מודולרי עם Samplomatic ו-Executor​

גישת ה-PNA נבנית על ארכיטקטורת מיתון שגיאות מודולרית ב-Qiskit. ארכיטקטורה זו משתמשת בספריית samplomatic יחד עם המחלקות QuantumProgram ו-Executor (שנוספו ל-Qiskit Runtime ב-qiskit-ibm-runtime גרסה v0.47.0) כדי להפוך טכניקות כמו ספיגת רעש מועברת ופיצול פאולי לניתנות להרכבה ולשימוש חוזר בין ניסויים שונים.

במקום להטמיע את לוגיקת המיתון בתוך הגדרת המעגל עצמה, המיתון מבוטא באופן דקלרטיבי דרך תיוגי samplomatic ומנוהל תוכניתית דרך Executor, שקובע כיצד מעגלים אקראיים נוצרים, מורצים, ומעובדים לאחר מכן.

במדריך הזה אנחנו מיישמים Qiskit pattern כדי להדגים כיצד PNA יכול להעביר ערוצי רעש פאולי הפוכים ולשנות את התצפית בהתאם, כדי לשפר את הערכת ערכי התוחלת ב-QPU-ים רועשים.

סקירת זרימת העבודה​

  • שלב 1: מיפוי לבעיה הקוונטית

    • בניית מודל איזינג-מוקפץ (kicked-Ising) מוטרטר בהשתקפות (mirrored Trotterized) ותצפית יעד.
  • שלב 2: אפיון והעברה של רעש

    • שימוש ב-samplomatic כדי לזהות ולתייג שכבות דו-קיוביטיות ומדידות ייחודיות במעגל.

    • למידת הרעש המשפיע על כל שכבה ייחודית באמצעות NoiseLearnerV3.

    • מיפוי כל תיוג InjectNoise למודל הרעש הנלמד המתאים לו.

    • שימוש ב-qiskit-addon-pna כדי להעביר את ערוצי הרעש ההפוכים קדימה דרך המעגל ולספוג אותם לתוך התצפית היעד.

  • שלב 3: הרצת ניסויים קוונטיים

    • הגדרת QuantumProgram כדי לציין דגימה אקראית דרך samplex ולהריץ את הניסויים על ה-backend באמצעות ה-Executor.
  • שלב 4: שחזור וניתוח תוצאות

    • השוואת אסטרטגיות מיתון (PNA, PNA+TREX, PNA+PS, PNA+PS+TREX) והמחשת השיפור לעומת תוצאות בלתי-ממותנות.

דרישות​

לפני שמתחילים במדריך הזה, ודאו שיש לכם מותקן את הבאים:

  • Qiskit SDK גרסה v2.2 ומעלה, עם תמיכת ויזואליזציה

  • Qiskit Runtime גרסה v0.47 ומעלה (pip install qiskit-ibm-runtime)

  • Samplomatic גרסה v0.13 ומעלה (pip install samplomatic)

  • תוסף (addon) PNA של Qiskit (pip install qiskit-addon-pna)

  • כלי עזר (utils) של Qiskit addon (pip install qiskit-addon-utils)

הגדרה​

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

_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_pna": "qiskit-addon-pna", "qiskit_addon_utils": "qiskit-addon-utils", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "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")
from qiskit import QuantumCircuit
from qiskit.quantum_info import Pauli, SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager, PassManager
from qiskit_ibm_runtime import (
QiskitRuntimeService,
QuantumProgram,
Executor,
NoiseLearnerV3,
)
from qiskit_addon_utils.exp_vals.measurement_bases import (
get_measurement_bases,
)
from qiskit_addon_utils.exp_vals.expectation_values import (
executor_expectation_values,
)
from qiskit_addon_utils.noise_management import trex_factors
from qiskit_addon_utils.noise_management.post_selection import PostSelector
from qiskit_addon_utils.noise_management.post_selection.transpiler.passes import (
AddPostSelectionMeasures,
AddSpectatorMeasures,
)
from qiskit_addon_pna import generate_noise_mitigating_observable
import samplomatic
from samplomatic.transpiler import generate_boxing_pass_manager
from samplomatic.annotations import InjectNoise
from samplomatic.utils import get_annotation, find_unique_box_instructions
import numpy as np
import matplotlib.pyplot as plt

# Selects a connected chain of low-error qubits on the target backend.
# The mirrored kicked-Ising circuit is a 1D chain, so we only need a line
# of connected physical qubits; the helper walks the backend's coupling map
# and grows a chain along the lowest-error two-qubit edges, so it works for
# any backend rather than relying on a hardcoded layout.
def find_qubit_chain(backend, length):
"""Find a connected chain of ``length`` physical qubits on ``backend``.

The chain is grown greedily along the lowest-error two-qubit edges, so it
favors better-performing qubits. Because the mirrored kicked-Ising circuit
is a 1D chain, a connected line is all we need.
"""
target = backend.target

# Identify the native two-qubit gate and build a per-edge error lookup.
two_qubit_gate = next(
name
for name in target.operation_names
if target[name]
and all(q is not None and len(q) == 2 for q in target[name])
)
edge_error = {
frozenset(qargs): (
1.0 if props is None or props.error is None else props.error
)
for qargs, props in target[two_qubit_gate].items()
}

graph = backend.coupling_map.graph.to_undirected(multigraph=False)
neighbors = {n: list(graph.neighbors(n)) for n in graph.node_indices()}

def first_chain_from(start):
path, visited = [start], {start}

def grow():
if len(path) == length:
return True
node = path[-1]
order = sorted(
neighbors[node],
key=lambda m: edge_error.get(frozenset((node, m)), 1.0),
)
for nxt in order:
if nxt not in visited:
visited.add(nxt)
path.append(nxt)
if grow():
return True
path.pop()
visited.remove(nxt)
return False

return path if grow() else None

def chain_cost(path):
return sum(
edge_error.get(frozenset((path[i], path[i + 1])), 1.0)
for i in range(len(path) - 1)
)

# Try low-degree qubits first (the natural ends of long chains) and keep
# the lowest-error chain found.
best_path, best_cost = None, float("inf")
for start in sorted(neighbors, key=lambda n: len(neighbors[n])):
chain = first_chain_from(start)
if chain is not None and (cost := chain_cost(chain)) < best_cost:
best_path, best_cost = chain, cost

if best_path is None:
raise ValueError(
f"Could not find a connected chain of {length} qubits "
f"on '{backend.name}'."
)
return best_path

דוגמה בסימולטור בקנה מידה קטן​

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

  • NoiseLearnerV3 מאפיין באופן ניסויי את ערוץ הרעש הפאולי-לינדבלד הדליל המחובר לכל שכבה דו-קיוביטית ייחודית במעגל שעבר טרנספילציה. בסימולטור נטול-רעש אין רעש ללמוד, וערוץ ההופכי המועבר יהיה זהות.

  • הפרימיטיב Executor דוגם את המעגלים האקראיים, שעברו twirling, שנוצרו על ידי samplomatic ב-backend.

באופן עקרוני ניתן להחליף במודל רעש סינתטי. לדוגמה, ניתן לצרף הוראות PauliLindbladError עם Qiskit Aer ולהעביר את המעגל הרועש המתקבל ישירות אל generate_noise_mitigating_observable. עם זאת, זה רק מאמת את החישוב הקלאסי מול רעש שהזרקתם בעצמכם, ומטשטש את המטרה של הטכניקה. מסיבה זו אנחנו מדלגים על דוגמת הסימולטור ומדגימים את זרימת העבודה המלאה של PNA ישירות על חומרה, כאשר כל שלב של ה-Qiskit pattern מפורט למטה.

דוגמת חומרה בקנה מידה גדול​

אנחנו כעת מריצים את זרימת העבודה המלאה של PNA על מודל איזינג המוקפץ (kicked Ising) בעל 30 אתרים המורץ על חומרת IBM Quantum®, בהתאם לארבעת השלבים של Qiskit pattern.

שלב 1: מיפוי לבעיה קוונטית​

יצירת מעגל טרוטר משוקף (mirrored) ותצפית

עבור ניסוי זה, נחקור את הדינמיקה בזמן של מודל איזינג המוקפץ (kicked Ising) בעל 30 אתרים על שרשרת ספין חד-ממדית. ההמילטוניאן הנבחן הוא:

H=−J∑⟨i,j⟩ZiZj+h∑iXiH = -J\sum\limits_{\langle i,j \rangle} Z_iZ_j + h\sum\limits_iX_i,

כאשר J>0J>0 מתאר את הצימוד של ספינים שכנים קרובים, i<ji<j, והשדה הרוחבי הגלובלי, hh, מוגדר ל-π8\frac{\pi}{8}. ככל ש-hh רחוק יותר מזווית קליפורד (כלומר, θ=nπ2,n∈Z\theta=n\frac{\pi}{2}, n \in \mathbb{Z}), כך נעשה קשה יותר להעביר את מחוללי האנטי-רעש דרך המעגל.

עבור בחירת התצפית, נשקול את המגנטיזציה החד-אתרית הממוצעת, 1N∑i=1N⟨zi⟩\frac{1}{N} \sum_{i=1}^{N} \langle z_i \rangle, כאשר NN הוא מספר האתרים.

num_qubits = 30
num_trotter_steps = 10
rx_angle = np.pi / 8

# Avg single-site magnetization
id_pauli = Pauli("I" * num_qubits)
observable = (
SparsePauliOp([id_pauli.dot(Pauli("Z"), [i]) for i in range(num_qubits)])
/ num_qubits
)

# Implement Trotterized kicked-Ising model
circuit = QuantumCircuit(num_qubits)
for _step in range(num_trotter_steps):
circuit.rx(rx_angle, range(num_qubits))
for first_qubit in (1, 2):
for idx in range(first_qubit, num_qubits, 2):
# equivalent to Rzz(-pi/2):
circuit.sdg([idx - 1, idx])
circuit.cz(idx - 1, idx)
# Append the inverse circuit to complete the mirroring
circuit.compose(circuit.inverse(), inplace=True)
circuit.measure_active()
circuit.draw("mpl", fold=-1)

Output of the previous code cell

שלב 2: אופטימיזציה של הבעיה להרצת חומרה​

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

  1. הוא מכבד את מערך השערים המקורי (native) והקישוריות של ה-backend. טרנספילציה ממפה את המעגל הלוגי למעגל ISA הנתמך על ידי ה-backend היעד. זה מבטיח שכל אינטראקציה של שער וקיוביט ניתנת למימוש פיזי.

  2. אנחנו יכולים לאפיין את הרעש ברמת שכבות המעגל. PNA מסתמך על למידה והעברה של ערוצי רעש הפוכים. כדי לעשות זאת ביעילות, אנחנו מפרקים את המעגל שעבר טרנספילציה לשכבות דו-קיוביטיות ייחודיות "מתוחמות" (boxed). תיחומים אלה מאפשרים לנו לשייך כל שכבה של המעגל למודל הרעש הנלמד שלה.

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

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

חיבור אל ה-backend וטרנספילציה למעגל ISA​

ראשית, אנחנו מאתחלים את IBM Quantum Compute Service ובוחרים backend. אמתו באמצעות החשבון שלכם על ידי ביצוע ההוראות לשמירת פרטי ההזדהות שלכם, שלאחריהן QiskitRuntimeService() יאסוף אותם באופן אוטומטי.

# Initialize IBM Quantum Compute Service using your saved credentials
service = QiskitRuntimeService()

backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=num_qubits
)
# Re-fetch with fractional gates enabled (least_busy does not forward this)
# Fractional gates are enabled so the non-Clifford Rx rotations are supported natively.
backend = service.backend(backend.name, use_fractional_gates=True)
print(f"Selected backend: {backend.name}")
Selected backend: ibm_fez

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

באמצעות פונקציית העזר find_qubit_chain שהוגדרה בסעיף ההגדרה, אנחנו בוחרים קו של num_qubits קיוביטים פיזיים מחוברים. לאחר מכן אנחנו מבצעים טרנספילציה עם optimization_level=0 ומצמידים את השרשרת הזו כ-initial_layout, מה שמשמר בדיוק את מבנה שכבת השער הדו-קיוביטי של המעגל המשוקף. המבנה הזה הוא מה שעליו מסתמכים שלבי התיחום ולמידת הרעש, כך שיש להימנע כאן מרמת אופטימיזציה גבוהה יותר (שהייתה מבטלת את השערים המשוקפים).

# Find a connected, low-error chain of qubits on the chosen backend
layout = find_qubit_chain(backend, num_qubits)

# Transpile the circuit for the target backend, pinning the chain as the layout.
# optimization_level=0 preserves the mirrored two-qubit-gate layers that the
# boxing and noise-learning steps rely on.
pm = generate_preset_pass_manager(
backend=backend, optimization_level=0, initial_layout=layout
)
isa_circuit = pm.run(circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)
isa_circuit.draw("mpl", fold=-1)

Output of the previous code cell

סיבוב אקראי (twirling) של שכבות השער הדו-קיוביטי והמדידות, ואיתור שכבות ייחודיות​

אנחנו משתמשים ב-samplomatic כדי לתחום את המעגל ולזהות שכבות דו-קיוביטיות ייחודיות. תיחום (box) הוא מבנה שמקבץ הוראות יחד כך שכוונות או תיוגים מסוימים יוכלו מאוחר יותר להיות מיושמים באופן אחיד על כל השערים בתוך אותו תיחום.

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

  • מקבצת את כל השכבות הדו-קיוביטיות במעגל,

  • מיישמת את התיוגים Twirl ו-ChangeBasis על שכבות אלה,

  • מקבצת פעולות מדידה לתוך קטעים מתוחמים משלהן, ו

  • מיישמת את התיוג InjectNoise על כל שכבה דו-קיוביטית.

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

אפשרויות התצורה המרכזיות הן:

  • enable_gates/enable_measures: True: מתחם את כל שכבות השער הדו-קיוביטיות ואת המדידות הסופיות. שערים חד-קיוביטיים נותרים "מלובשים משמאל" (left-dressed) בתוך התיחומים.

  • measure_annotations: all: כולל תיוגים Twirl ו-ChangeBasis בתיחום המדידה.

  • twirling_strategy: active: מבצע סיבוב אקראי (twirling) על כל הקיוביטים הפעילים בכל תיחום המכיל שערים שזורים (entangling).

  • inject_noise_targets: gates: מוסיף תיוגי InjectNoise לכל התיחומים המתויגים ב-Twirl המכילים שערים שזורים.

  • inject_noise_strategy: uniform_modification: מקנה קנה מידה שווה לכל שכבות הרעש לאורך המעגל.

# Box up circuit with Twirl and InjectNoise annotations
pm = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
measure_annotations="all",
twirling_strategy="active",
inject_noise_targets="gates",
inject_noise_strategy="uniform_modification",
)
boxed_circuit = pm.run(isa_circuit)
draw_circ = QuantumCircuit(boxed_circuit.num_qubits)
draw_circ.append(boxed_circuit.data[0], qargs=boxed_circuit.data[0].qubits)
draw_circ.append(boxed_circuit.data[1], qargs=boxed_circuit.data[1].qubits)
draw_circ.draw("mpl", fold=-1, scale=0.3, idle_wires=False)

Output of the previous code cell

יצירת מעגל התבנית (template circuit) וה-samplex, שמגדירים כיצד המעגל יידגם.

כאן אנחנו גם מוסיפים מדידות צופה (spectator) ובחירה-לאחר-מכן, שנחוצות כדי לבצע בחירה-לאחר-מכן על הדגימות שנפלטות מה-Executor.

# Build template circuit and samplex for later use with the "Executor"
template_circuit, samplex = samplomatic.build(boxed_circuit)

# Add post-selection instructions to the template circuit
post_selection_pm = PassManager(
[
AddSpectatorMeasures(backend.coupling_map),
AddPostSelectionMeasures(x_pulse_type="rx"),
]
)
template_circuit = post_selection_pm.run(template_circuit)
draw_circ = template_circuit.copy_empty_like()
draw_circ.data = template_circuit.data[:324]
draw_circ.draw("mpl", fold=-1, scale=0.3, idle_wires=False)

Output of the previous code cell

למידת הרעש באמצעות NoiseLearnerV3​

לפני שנוכל ליישם PNA למיתון שגיאות, אנחנו צריכים ראשית לאפיין את הרעש הפועל על כל שכבה דו-קיוביטית ייחודית ועל שכבת המדידה במעגל שלנו. לשם כך, אנחנו משתמשים בתוכנית NoiseLearnerV3 כדי ללמוד באופן ניסויי מודלי רעש עבור כל שכבה שזוהתה קודם לכן. הלומד (learner) מבצע ניסויים בסגנון בנצ'מרקינג שמעריכים את ערוץ הרעש המשפיע על כל שכבה ומחזירים אובייקט תוצאה המכיל את המודל הנלמד.

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

ישנם כמה פרמטרים מרכזיים ששולטים באופן שבו הרעש נלמד:

  • num_randomizations: מספר המעגלים האקראיים המשמשים לכל תצורת למידה.

  • shots_per_randomization: מספר היריות (shots) שנלקחות לכל מעגל למידה אקראי.

  • layer_pair_depths: עומקי המעגל (נמדדים במספר זוגות) לשימוש בניסויי למידה.

  • post_selection: מאפשר בחירה-לאחר-מכן מבוססת-קשתות באמצעות שערי rx ליישום פולסים לאחר-מדידה.

# Noise learning parameters
num_randomizations_nl = 64
shots_per_randomization_nl = 128

# Match the deepest noise-learning depth to the depth of the circuit being
# mitigated. ``layer_pair_depths`` are measured in layer pairs (a layer + its
# inverse), so the deepest value is half the circuits's two-qubit-layer depth.
# Learning to this depth improves the quality of the mitigation.

depth_2q = isa_circuit.depth(lambda instr: instr.operation.num_qubits == 2)
max_layer_pair_depth = depth_2q // 2

# Use a fixed schedule of learning depths, but drop any that exceed the circuit's
# depth and always cap the deepest value at ``max_layer_pair_depth`` so we never
# learn deeper than the circuit being mitigated
candidate_depths = [1, 2, 4, 8, 12, 16, 24, 32, 40, 48]
layer_pair_depths = sorted(
{d for d in candidate_depths if d < max_layer_pair_depth}
| {max_layer_pair_depth}
)

# Find the unique instructions (layers) from the boxed-up circuit
unique_2q_layers_and_meas = find_unique_box_instructions(
boxed_circuit, normalize_annotations=None, undress_boxes=True
)

# Configure and run the noise learner on the unique layers.
# Options can be passed directly as a dictionary.
noise_learner_options = {
"num_randomizations": num_randomizations_nl,
"shots_per_randomization": shots_per_randomization_nl,
"layer_pair_depths": layer_pair_depths,
"post_selection": {
"enable": True,
"strategy": "edge",
"x_pulse_type": "rx",
},
"environment": {"job_tags": ["TUT_PNA"]},
}

noise_learner = NoiseLearnerV3(backend, noise_learner_options)
noise_learner_job = noise_learner.run(unique_2q_layers_and_meas)
noise_learner_result = noise_learner_job.result()

המחשת קצבי הרעש הנלמדים​

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

עבור כל שכבה נלמדת:

  • אנחנו ממירים את מודל הרעש לרשימה דלילה של שלשות (pstr, qubits, rate), כאשר pstr הוא מחרוזת הפאולי הפועלת על הקיוביטים הנתונים ו-rate הוא קצב השגיאה המשויך.
  • אנחנו מפרידים את הקצבים לאיברים חד-קיוביטיים (len(pstr) == 1) ודו-קיוביטיים (len(pstr) == 2).
  • רשימות הקצבים ממוינות לאחר מכן, וערכי החציון שלהן מחושבים.

אנחנו מציגים בגרף את התפלגויות קצב הרעש החד-קיוביטי (אדום) והדו-קיוביטי (כחול) בקנה מידה לוגריתמי, כאשר ערכי החציון שלהם מסומנים בקווים אנכיים, כך שנוכל להשוות את הגדלים היחסיים של מחוללי הפאולי-לינדבלד הנלמדים. הסדר של הקצבים החד- והדו-קיוביטיים תלוי בהתקן ובשכבות הספציפיות שמאופיינות; בהרצה הזו, המחוללים החד-קיוביטיים (משקל-1) נושאים את קצב החציון הגבוה יותר.

hw_rates_1q = []
hw_rates_2q = []
for nlr in noise_learner_result[:2]:
plm_list = nlr.to_pauli_lindblad_map().to_sparse_list()
hw_rates_1q += [
rate for (pstr, qubits, rate) in plm_list if len(pstr) == 1
]
hw_rates_2q += [
rate for (pstr, qubits, rate) in plm_list if len(pstr) == 2
]
hw_rates_1q = sorted(hw_rates_1q)
hw_rates_2q = sorted(hw_rates_2q)
median_1q = hw_rates_1q[len(hw_rates_1q) // 2]
median_2q = hw_rates_2q[len(hw_rates_2q) // 2]
fig, ax = plt.subplots(1, 1, figsize=(14, 5))
ax.scatter(
(hw_rates_1q),
[(i) / (len(hw_rates_1q) - 1) for i in range(len(hw_rates_1q))],
color="red",
label="1q rates",
)
ax.set_xscale("log")
ax.set_ylim(0, 1.1)
ax.vlines(median_1q, 0, 1, color="red")
ax.text(median_1q * 1.1, 0.1, f"{median_1q:.2e}")
ax.scatter(
(hw_rates_2q),
[(i) / (len(hw_rates_2q) - 1) for i in range(len(hw_rates_2q))],
color="blue",
label="2q rates",
)
ax.set_xscale("log")
ax.set_ylim(0, 1.1)
ax.vlines(median_2q, 0, 1, color="blue")
ax.text(median_2q * 1.1, 0.2, f"{median_2q:.2e}")
ax.set_title("Learned noise rates")
ax.set_xlabel("Noise rate")
ax.set_yticks([])
plt.legend()
<matplotlib.legend.Legend at 0x125336120>

Output of the previous code cell

שיוך תיחומי מעגל לרעש נלמד​

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

ההנחיה InjectNoise היא תיוג samplomatic המשתמש בשערי "הלבשה" חד-קיוביטיים (single-qubit dressers) כדי להזריק רעש למעגל בצורה מבוקרת וניתנת להגדרה. הוא מאפשר מידול רעש מודולרי בין שכבות שונות.

כל תיוג InjectNoise כולל:

  • InjectNoise.ref - מזהה ייחודי לתיוג. זה משמש את האובייקט samplex כדי להקצות נכון את מודל הרעש המתאים.
  • InjectNoise.modifier_ref (אופציונלי) - הפניה משנית שמאפשרת לשנות את קנה המידה של מודל הרעש המוקצה באמצעות מקדם כפלי.

בשלב זה, אנחנו בונים מיפוי מכל InjectNoise.ref למודל הרעש הנלמד המתאים לו (PauliLindbladMap). המיפוי הזה מבטיח שכל שכבת שערי שזירה (entangling gates) במעגל תותאם למודל הרעש המתאים, כך שאפקטי הרעש יוחלו בצורה מדויקת במהלך הדגימה ובשלבי מיתון הרעש שבאים לאחר מכן.

# map inject noise refs to pauli lindblad maps
refs_to_noise_models = {}
for instruction, result in zip(
unique_2q_layers_and_meas, noise_learner_result, strict=False
):
if inject_noise_annot := get_annotation(
instruction.operation, InjectNoise
):
refs_to_noise_models[inject_noise_annot.ref] = (
result.to_pauli_lindblad_map()
)

העברת התצפית דרך אנטי-הרעש הנלמד​

כפי שנדון לעיל, זה נעשה בשני שלבים. ראשית, אנחנו מעבירים מחולל אנטי-רעש לסוף המעגל. לאחר מכן, אנחנו מעבירים את התצפית דרך המחולל המתפתח הזה. תהליך זה חוזר על עצמו עבור כל מחולל אנטי-רעש במעגל. במימוש הזה, כל מחולל בשכבה נתונה מועבר לסוף המעגל במקביל. בנוסף, שימוש ב-Python multiprocessing מאפשר לבצע גם את ההעברה קדימה (forward-propagation) של האנטי-רעש וגם את ההעברה אחורה (back-propagation) של התצפית במקביל. זה מונע הצטברות של מחוללים מתפתחים בזיכרון וגם ממקסם את משאבי המחשוב.

כשמריצים PNA, תמיד צריך לספק מעגל רועש ותצפית. אם המעגל הרועש שלכם הוא מעגל מתוחם עם תיוגי InjectNoise, עליכם לספק את המיפוי שיצרנו בשלב הקודם. ניתן גם להעביר מעגל שאינו מתוחם המכיל הוראות PauliLindbladError מ-qiskit-aer. במקרה כזה, אין צורך לספק את refs_to_noise_models. בנוסף לקלטים העיקריים, יש לקחת בחשבון גם את הבאים:

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

  • max_obs_terms: מספר האיברים לשמור בתצפית ממתנת-הרעש, O~\tilde{O}, בעת ההעברה שלה אחורה דרך האנטי-רעש המתפתח. ערכים גדולים יותר בדרך כלל מגדילים את הדיוק, אך זה לא מובטח לקרות בצורה מונוטונית.

  • num_processes: מספר הליבות להקדיש לתהליך. זכרו, המחוללים מופצים קדימה ומוחלים על התצפית במקביל.

  • search_step: שלב ההעברה אחורה משתמש בשיטה חמדנית כדי לבצע הצמדה (conjugation) מקורבת של שני אופרטורים בבסיס פאולי. ניתן להאיץ שיטה זו על ידי הגדלת search_step. עיינו בתיעוד pauli-prop למידע נוסף.

  • num_to_measure: בעוד שמשתנה זה אינו קלט ל-generate_noise_mitigating_observable, אנחנו משתמשים בו כדי לשלוט בכמה איברים מתוך O~\tilde{O} אנחנו בעצם רוצים למדוד. כאן אנחנו מודדים רק את 30 האיברים המובילים, שהם האיברים המקוריים בתצפית שלנו. האיברים כעת עברו שינוי קנה מידה כך שמדידתם משיגה השפעה של מיתון רעש השער הנלמד. למרות שאנחנו מודדים רק 30 איברים מתוך O~\tilde{O}, לרוב עדיין שימושי לאפשר לו לגדול, מכיוון שזה מגדיל את הדיוק של מקדמי שינוי קנה המידה של האיברים המובילים.

# PNA parameters
num_processes = 8
max_err_terms = 10_000
max_obs_terms = 10_000
num_to_measure = num_qubits

obs_tilde_isa = generate_noise_mitigating_observable(
boxed_circuit,
isa_observable,
refs_to_noise_models,
max_err_terms=max_err_terms,
max_obs_terms=max_obs_terms,
num_processes=num_processes,
print_progress=True,
search_step=8,
)
p_2_v = {p: v for v, p in enumerate(layout)}
obs_tilde_virtual = SparsePauliOp.from_sparse_list(
[
(pstr, [p_2_v[p] for p in p_qubits], coeff)
for (pstr, p_qubits, coeff) in obs_tilde_isa.to_sparse_list()
],
num_qubits=num_qubits,
)
obs_tilde_virtual = obs_tilde_virtual[
np.argsort(np.abs(obs_tilde_virtual.coeffs))[::-1]
][:num_to_measure]
Finished! 13740 / 13740 generators propagated.
obs_tilde_isa = obs_tilde_isa[np.argsort(np.abs(obs_tilde_isa.coeffs))][::-1]
plt.xscale("log")
plt.yscale("log")
plt.title(r"$\tilde{O}$ coeff magnitudes")
plt.ylabel("Magnitude")
plt.xlabel("Pauli term index")
plt.plot(np.abs(obs_tilde_isa.coeffs), ".")
[<matplotlib.lines.Line2D at 0x124b0a120>]

Output of the previous code cell

המרת בסיסי המדידה לצורה קנונית​

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

בנוסף למציאת קבוצה של בסיסי מדידה פאוליים, עלינו למפות את איברי הפאולי הללו לצורה הקנונית הצפויה על ידי ה-Executor. למידע נוסף על סדר קיוביטים קנוני, בקרו בתיעוד Samplomatic.

meas_box = boxed_circuit.data[-1]
canonical_qubits = [
idx
for idx, qubit in enumerate(boxed_circuit.qubits)
if qubit in meas_box.qubits
]
c_2_p = {
c: p for c, p in enumerate(canonical_qubits)
} # canonical -> physical
p_2_v = {p: v for v, p in enumerate(layout)} # physical -> virtual
c_2_v = {c: p_2_v[p] for c, p in c_2_p.items()} # canonical -> virtual
meas_bases, bases_reverser = get_measurement_bases(obs_tilde_virtual)
meas_bases_canonical = [
np.array([base[c_2_v[c]] for c in range(num_qubits)], dtype=np.uint8)
for base in meas_bases
]

שלב 3: הרצת ניסויים קוונטיים​

ציון כיצד לדגום ב-QuantumProgram​

כעת אנחנו מגדירים את ה-QuantumProgram, שמשמש כמכל המרכזי לכל המעגלים והגדרות הדגימה שיורצו על ידי ה-Executor. אובייקט זה מגדיר כיצד מופעים מעגליים אקראיים נוצרים, מקובצים ומורצים כדי לייצר את תוצאות המדידה המשמשות ב-PNA.

QuantumProgram יכול להכיל מספר פריטים, כל אחד מורכב ממעגל תבנית ואובייקט samplex מתאים המגדיר כיצד מיושמים אקראונים (randomizations). הפשטה זו מאפשרת ל-Executor לטפל בזרימת העבודה כולה כתוכנית יחידה ומודולרית — מיצירת מעגל אקראי ועד לאיסוף ואיגום ירי (shots).

בשלב זה, אנחנו יוצרים QuantumProgram שמריץ את ניסוי ה-PNA שלנו באמצעות מעגל התבנית וה-samplex שבנינו קודם. ההגדרה כוללת את האלמנטים הבאים:

  • template_circuit: המעגל המכיל את כל השערים הדרושים ליישום כל האקראונים הרצויים (מסיבוב אקראי (twirling), פרמטרים, וכן הלאה).

  • samplex: אובייקט המגדיר התפלגות הסתברות על פני כל האקראונים האפשריים של המעגל שממנה דוגמים.

  • samplex_arguments: קשירות הדרושות כדי להגדיר במלואו את ה-samplex

    • basis_changes: כאן אנחנו מציינים קבוצת בסיסים למדידה שתכסה את כל איברי הפאולי בתצפית הנמדדת.

    • noise_scales.ref: אנחנו מגדירים את קנה המידה של כל שכבת רעש ל-0.0 כדי למנוע הזרקת רעש נוסף לדגימות שלנו.

    • pauli_lindblad_maps: נדרש אם מועברים noise_scales. זה פשוט ממפה שכבות רעש למודל הרעש המשויך.

  • shape: כפולת צורה (shape tuple) להרחבת הצורה המשתמעת המוגדרת על ידי samplex_arguments. צירים לא-טריוויאליים שמוצגים על ידי הרחבה זו מונים אקראונים.

# Control the # of shots during execution
shots_per_randomization_exec = 64
num_randomizations_exec = 6144

# Zero out the noise to prevent noise from being injected during execution.
# We only added InjectNoise annotations so PNA could associate the noise
# to layers in the circuit
samplex_inputs = {f"noise_scales.{ref}": 0.0 for ref in refs_to_noise_models}
samplex_inputs |= {"pauli_lindblad_maps": refs_to_noise_models}

# Specify the bases to measure. The samplex exposes one basis-change input per
# ChangeBasis-annotated box; here a single all-Z basis covers every term. We
# look up the basis-change interface name rather than hardcoding an index, since
# the name depends on the circuit's box structure.
bases_broadcastable = np.expand_dims(np.array(meas_bases_canonical), axis=1)
samplex_inputs |= {
spec.name: bases_broadcastable
for spec in samplex.inputs().get_specs(r"^basis_changes\.")
}

# Convert samplex_inputs into a dict to pass to QuantumProgram
samplex_arguments = (
samplex.inputs().make_broadcastable().bind(**samplex_inputs)
)

# Instantiate the QuantumProgram with the specified parameters
program = QuantumProgram(shots=shots_per_randomization_exec)
program.append_samplex_item(
circuit=template_circuit,
samplex=samplex,
samplex_arguments=samplex_arguments,
shape=(num_randomizations_exec,),
)

דגימת המעגל באמצעות ה-Executor​

עכשיו לאחר שהגדרנו את ה-QuantumProgram שלנו, הרצת הניסוי היא פשוטה. אנחנו פשוט יוצרים מופע של אובייקט ה-Executor, מספקים לו את ה-backend, ומריצים את התוכנית.

# Execute (sample) the circuit
executor = Executor(backend)
job_exec = executor.run(program)
exec_results = job_exec.result()

שלב 4: שחזור וניתוח תוצאות​

כדי לחשב ערך תוחלת ממותן-שגיאה, אנחנו עושים את הבאים:

  • חישוב מקדמי שינוי קנה המידה של TREX בהתבסס על הרעש הנלמד שמשפיע על המדידות,

  • יצירת מסכה לשמירה על דגימות שנבחרו-לאחר-מכן (post-selected) בלבד, וכן

  • שימוש בפונקציית executor_expectation_values מ-qiskit-addon-utils כדי לשלב את כל הנתונים לערך תוחלת ממותן-שגיאה.

# Computing the TREX factors
measurement_noise_map = noise_learner_result[2].to_pauli_lindblad_map()
trex_rescale_factors = trex_factors(measurement_noise_map, bases_reverser)

# Post-select the results
post_selector = PostSelector.from_circuit(
circuit=template_circuit, coupling_map=backend.coupling_map
)

# Compute the ps mask for filtering results
mask = post_selector.compute_mask(exec_results[0], strategy="edge")

# Compute expvals using post selected results
results = executor_expectation_values(
exec_results[0]["meas"],
bases_reverser,
meas_basis_axis=0,
avg_axis=1,
measurement_flips=exec_results[0]["measurement_flips.meas"],
pauli_signs=exec_results[0].get("pauli_signs", None),
postselect_mask=mask,
rescale_factors=trex_rescale_factors,
)

השוואת אסטרטגיות מיתון: PNA, PNA+TREX, PNA+PS, PNA+PS+TREX​

אנחנו מחשבים וממחישים ערכי תוחלת עבור מספר וריאנטים של מיתון באמצעות תוצאות ה-Executor.

bases_reverser_unmit = {Pauli("Z" * num_qubits): [observable]}
args = [
(bases_reverser_unmit, None, None),
(bases_reverser, None, None),
(bases_reverser, None, trex_rescale_factors),
(bases_reverser, mask, None),
(bases_reverser, mask, trex_rescale_factors),
]

evs = []
for reverser, postsel_mask, factors in args:
# Compute expvals using post selected results
res_ps = executor_expectation_values(
exec_results[0]["meas"],
reverser,
meas_basis_axis=0,
avg_axis=1,
measurement_flips=exec_results[0]["measurement_flips.meas"],
pauli_signs=exec_results[0].get("pauli_signs", None),
postselect_mask=postsel_mask,
rescale_factors=factors,
)
res_ps = np.array(res_ps)
evs.append(res_ps[:, 0][0])

experiments = ["PNA", "PNA+TREX", "PNA+PS", "PNA+PS+TREX"]
colors = ["#d9d9d9", "#b0b0b0", "#7f7f7f", "#4c4c4c"]
plt.bar(experiments, evs[1:], color=colors)
plt.axhline(y=1, color="green", linestyle="--", linewidth=2, label="Ideal")
plt.axhline(
y=evs[0], color="red", linestyle="--", linewidth=2, label="Unmitigated"
)
plt.ylabel("Expectation value", fontsize=14)

plt.title(
r"30q Mirrored Ising, 10 Trotter steps, $\theta_{rx}=\frac{\pi}{8}$",
fontsize=14,
)
plt.legend(loc="upper left", bbox_to_anchor=(1.05, 1), borderaxespad=0.0)
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

Output of the previous code cell

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

  • הוספת שקלול מחדש של TREX (PNA+TREX) משפרת מעט את האומדן על ידי תיקון חוסר איזון בדגימה במעגלים האקראיים.

  • בחירה-לאחר-מכן (PNA+PS) מספקת שיפור בולט יותר על ידי סינון תוצאות מדידה לא עקביות שככל הנראה נובעות משגיאות שיוריות.

  • לבסוף, שילוב שניהם (PNA+PS+TREX) מניב את התוצאה המדויקת ביותר, כמעט תואמת לערך האידיאלי, ומראה כיצד אסטרטגיות מיתון אלה מחזקות זו את זו.

בסך הכל, ההשוואה מדגישה ש-PNA משמש כבסיס איתן לאומדן ערך תוחלת מודע-רעש, בעוד ש-TREX ובחירה-לאחר-מכן מציעים שיפורים משלימים להשגת דיוק נוסף.

השלבים הבאים​

אם מצאתם עבודה זו מעניינת, ייתכן שתתעניינו בחומר הבא: