הפחתת שגיאות בקנה מידה של שירות עם הגברת שגיאות הסתברותית
אומדן שימוש: 16 דקות על מעבד Heron r2 (הערה: זהו אומדן בלבד. זמן הריצה בפועל עשוי להשתנות.)
רקע
מדריך זה מדגים כיצד להפעיל ניסוי הפחתת שגיאות בקנה מידה של שירות עם Qiskit Runtime, תוך שימוש בגרסה ניסויית של אקסטרפולציה לאפס רעש (ZNE) עם הגברת שגיאות הסתברותית (PEA).
מקור: Y. Kim et al. Evidence for the utility of quantum computing before fault tolerance. Nature 618.7965 (2023)
אקסטרפולציה לאפס רעש (ZNE)
אקסטרפולציה לאפס רעש (ZNE) היא טכניקת הפחתת שגיאות המסירה את השפעות הרעש הבלתי ידוע במהלך הרצת Circuit, כאשר ניתן לשנות את עוצמת הרעש בדרך ידועה.
הטכניקה מניחה שערכי הציפייה משתנים עם הרעש לפי פונקציה ידועה:
כאשר מפרמטר את עוצמת הרעש וניתן להגביר אותה. ניתן לממש ZNE בשלבים הבאים:
- הגברת רעש ה-Circuit עבור מספר גורמי רעש
- הרצת כל Circuit מוגבר-רעש למדידת
- אקסטרפולציה חזרה לגבול האפס-רעש

הגברת רעש עבור ZNE
האתגר המרכזי ביישום מוצלח של ZNE הוא לבנות מודל רעש מדויק עבור ערך הציפייה, ולהגביר את הרעש בדרך ידועה.
קיימות שלוש שיטות נפוצות להגברת שגיאות עבור ZNE:
| מתיחת פולס | קיפול Gate | הגברת שגיאות הסתברותית |
|---|---|---|
| הרחבת משך הפולס באמצעות כיול | חזרה על Gates במחזורי זהות | הוספת רעש על ידי דגימת ערוצי פאולי |
| Kandala et al. Nature (2019) | Shultz et al. PRA (2022) | Li & Benjamin PRX (2017) |
| עבור ניסויים בקנה מידה של שירות, הגברת שגיאות הסתברותית (PEA) היא הגישה האטרקטיבית ביותר: |
- מתיחת פולס מניחה שרעש ה-Gate פרופורציונלי למשכו, דבר שאינו נכון בדרך כלל. בנוסף, הכיול יקר מבחינת משאבים.
- קיפול Gate דורש גורמי מתיחה גדולים המגבילים מאוד את עומק ה-Circuit שניתן להריץ.
- PEA ניתנת ליישום על כל Circuit שניתן להריץ עם גורם הרעש המקורי (), אך דורשת לימוד מודל הרעש.
לימוד מודל הרעש עבור PEA
PEA מניחה את אותו מודל רעש מבוסס-שכבות כמו ביטול שגיאות הסתברותי (PEC); עם זאת, היא נמנעת מעומס הדגימה המתרחב אקספוננציאלית עם רעש ה-Circuit.
| שלב 1 | שלב 2 | שלב 3 |
|---|---|---|
| סיבוב פאולי של שכבות Gates דו-קיוביטיות | חזרה על זוגות זהות של שכבות ולמידת הרעש | גזירת ערך נאמנות (שגיאה לכל ערוץ רעש) |
![]() | ![]() |
מקור: E. van den Berg, Z. Minev, A. Kandala, and K. Temme, Probabilistic error cancellation with sparse Pauli-Lindblad models on noisy quantum processors arXiv:2201.09866
דרישות מוקדמות
לפני תחילת מדריך זה, ודאו שהדברים הבאים מותקנים:
- Qiskit SDK גרסה 1.0 ומעלה, עם תמיכה בויזואליזציה
- Qiskit Runtime גרסה 0.22 ומעלה (
pip install qiskit-ibm-runtime)
הגדרה
from __future__ import annotations
from collections.abc import Sequence
from collections import defaultdict
import numpy as np
import rustworkx
import matplotlib.pyplot as plt
from qiskit.circuit import QuantumCircuit, Parameter
from qiskit.circuit.library import CXGate, CZGate, ECRGate
from qiskit.providers import Backend
from qiskit.visualization import plot_error_map
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.quantum_info import SparsePauliOp
from qiskit.primitives import PubResult
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import EstimatorV2 as Estimator
שלב 1: מיפוי קלטים קלאסיים לבעיה קוונטית
יצירת Circuit מפורמטר של מודל איזינג
ראשית, בחרו Backend להרצה. הדגמה זו מתבצעת על Backend בעל 127 קיוביטים, אך ניתן לשנות זאת לכל Backend הזמין לכם.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
backend
<IBMBackend('ibm_kingston')>
פונקציות עזר לבניית Circuit
לאחר מכן, צרו מספר פונקציות עזר לבניית ה-Circuits עבור התפתחות הזמן ב-Trotter של מודל איזינג הרוחבי דו-ממדי, בהתאמה לטופולוגיית ה-Backend.
"""Trotter circuit generation"""
def remove_qubit_couplings(
couplings: Sequence[tuple[int, int]], qubits: Sequence[int] | None = None
) -> list[tuple[int, int]]:
"""Remove qubits from a coupling list.
Args:
couplings: A sequence of qubit couplings.
qubits: Optional, the qubits to remove.
Returns:
The input couplings with the specified qubits removed.
"""
if qubits is None:
return couplings
qubits = set(qubits)
return [edge for edge in couplings if not qubits.intersection(edge)]
def coupling_qubits(
*couplings: Sequence[tuple[int, int]],
allowed_qubits: Sequence[int] | None = None,
) -> list[int]:
"""Return a sorted list of all qubits involved in one or more couplings lists.
Args:
couplings: one or more coupling lists.
allowed_qubits: Optional, the allowed qubits to include. If None all
qubits are allowed.
Returns:
The intersection of all qubits in the couplings and the allowed qubits.
"""
qubits = set()
for edges in couplings:
for edge in edges:
qubits.update(edge)
if allowed_qubits is not None:
qubits = qubits.intersection(allowed_qubits)
return list(qubits)
def construct_layer_couplings(
backend: Backend,
) -> list[list[tuple[int, int]]]:
"""Separate a coupling map into disjoint 2-qubit gate layers.
Args:
backend: A backend to construct layer couplings for.
Returns:
A list of disjoint layers of directed couplings for the input coupling map.
"""
coupling_graph = backend.coupling_map.graph.to_undirected(
multigraph=False
)
edge_coloring = rustworkx.graph_bipartite_edge_color(coupling_graph)
layers = defaultdict(list)
for edge_idx, color in edge_coloring.items():
layers[color].append(
coupling_graph.get_edge_endpoints_by_index(edge_idx)
)
layers = [sorted(layers[i]) for i in sorted(layers.keys())]
return layers
def entangling_layer(
gate_2q: str,
couplings: Sequence[tuple[int, int]],
qubits: Sequence[int] | None = None,
) -> QuantumCircuit:
"""Generating a entangling layer for the specified couplings.
This corresponds to a Trotter layer for a ZZ Ising term with angle Pi/2.
Args:
gate_2q: The 2-qubit basis gate for the layer, should be "cx", "cz", or "ecr".
couplings: A sequence of qubit couplings to add CX gates to.
qubits: Optional, the physical qubits for the layer. Any couplings involving
qubits not in this list will be removed. If None the range up to the largest
qubit in the couplings will be used.
Returns:
The QuantumCircuit for the entangling layer.
"""
# Get qubits and convert to set to order
if qubits is None:
qubits = range(1 + max(coupling_qubits(couplings)))
qubits = set(qubits)
# Mapping of physical qubit to virtual qubit
qubit_mapping = {q: i for i, q in enumerate(qubits)}
# Convert couplings to indices for virtual qubits
indices = [
[qubit_mapping[i] for i in edge]
for edge in couplings
if qubits.issuperset(edge)
]
# Layer circuit on virtual qubits
circuit = QuantumCircuit(len(qubits))
# Get 2-qubit basis gate and pre and post rotation circuits
gate2q = None
pre = QuantumCircuit(2)
post = QuantumCircuit(2)
if gate_2q == "cx":
gate2q = CXGate()
# Pre-rotation
pre.sdg(0)
pre.z(1)
pre.sx(1)
pre.s(1)
# Post-rotation
post.sdg(1)
post.sxdg(1)
post.s(1)
elif gate_2q == "ecr":
gate2q = ECRGate()
# Pre-rotation
pre.z(0)
pre.s(1)
pre.sx(1)
pre.s(1)
# Post-rotation
post.x(0)
post.sdg(1)
post.sxdg(1)
post.s(1)
elif gate_2q == "cz":
gate2q = CZGate()
# Identity pre-rotation
# Post-rotation
post.sdg([0, 1])
else:
raise ValueError(
f"Invalid 2-qubit basis gate {gate_2q}, should be 'cx', 'cz', or 'ecr'"
)
# Add 1Q pre-rotations
for inds in indices:
circuit.compose(pre, qubits=inds, inplace=True)
# Use barriers around 2-qubit basis gate to specify a layer for PEA noise learning
circuit.barrier()
for inds in indices:
circuit.append(gate2q, (inds[0], inds[1]))
circuit.barrier()
# Add 1Q post-rotations after barrier
for inds in indices:
circuit.compose(post, qubits=inds, inplace=True)
# Add physical qubits as metadata
circuit.metadata["physical_qubits"] = tuple(qubits)
return circuit
def trotter_circuit(
theta: Parameter | float,
layer_couplings: Sequence[Sequence[tuple[int, int]]],
num_steps: int,
gate_2q: str | None = "cx",
backend: Backend | None = None,
qubits: Sequence[int] | None = None,
) -> QuantumCircuit:
"""Generate a Trotter circuit for the 2D Ising
Args:
theta: The angle parameter for X.
layer_couplings: A list of couplings for each entangling layer.
num_steps: the number of Trotter steps.
gate_2q: The 2-qubit basis gate to use in entangling layers.
Can be "cx", "cz", "ecr", or None if a backend is provided.
backend: A backend to get the 2-qubit basis gate from, if provided
will override the basis_gate field.
qubits: Optional, the allowed physical qubits to truncate the
couplings to. If None the range up to the largest
qubit in the couplings will be used.
Returns:
The Trotter circuit.
"""
if backend is not None:
try:
basis_gates = backend.configuration().basis_gates
except AttributeError:
basis_gates = backend.basis_gates
for gate in ["cx", "cz", "ecr"]:
if gate in basis_gates:
gate_2q = gate
break
# If no qubits, get the largest qubit from all layers and
# specify the range so the same one is used for all layers.
if qubits is None:
qubits = range(1 + max(coupling_qubits(layer_couplings)))
# Generate the entangling layers
layers = [
entangling_layer(gate_2q, couplings, qubits=qubits)
for couplings in layer_couplings
]
# Construct the circuit for a single Trotter step
num_qubits = len(qubits)
trotter_step = QuantumCircuit(num_qubits)
trotter_step.rx(theta, range(num_qubits))
for layer in layers:
trotter_step.compose(layer, range(num_qubits), inplace=True)
# Construct the circuit for the specified number of Trotter steps
circuit = QuantumCircuit(num_qubits)
for _ in range(num_steps):
circuit.rx(theta, range(num_qubits))
for layer in layers:
circuit.compose(layer, range(num_qubits), inplace=True)
circuit.metadata["physical_qubits"] = tuple(qubits)
return circuit
הגדרת קישורי שכבת השזירה
כדי לממש את סימולציית איזינג ב-Trotter, הגדירו שלוש שכבות של קישורי Gate דו-קיוביטי עבור המכשיר, אשר יחזרו על עצמן בכל אחד משלבי Trotter. שכבות אלו מגדירות את שלוש השכבות המסובבות שנדרש ללמוד עבורן את הרעש לצורך יישום ההפחתה.
layer_couplings = construct_layer_couplings(backend)
for i, layer in enumerate(layer_couplings):
print(f"Layer {i}:\n{layer}\n")
Layer 0:
[(2, 3), (4, 5), (6, 7), (8, 9), (10, 11), (12, 13), (14, 15), (16, 23), (18, 31), (19, 35), (20, 21), (25, 37), (26, 27), (28, 29), (33, 39), (36, 41), (38, 49), (42, 43), (45, 46), (47, 57), (51, 52), (53, 54), (56, 63), (58, 71), (59, 75), (61, 62), (64, 65), (66, 67), (68, 69), (72, 73), (76, 81), (79, 93), (82, 83), (84, 85), (86, 87), (88, 89), (91, 98), (94, 95), (97, 107), (99, 115), (100, 101), (102, 103), (105, 117), (108, 109), (110, 111), (113, 114), (116, 121), (118, 129), (123, 136), (124, 125), (126, 127), (130, 131), (132, 133), (135, 139), (138, 151), (142, 143), (144, 145), (146, 147), (152, 153), (154, 155)]
Layer 1:
[(0, 1), (3, 16), (5, 6), (7, 8), (11, 18), (13, 14), (17, 27), (21, 22), (23, 24), (25, 26), (29, 38), (30, 31), (32, 33), (34, 35), (39, 53), (41, 42), (43, 56), (44, 45), (47, 48), (49, 50), (51, 58), (54, 55), (57, 67), (60, 61), (62, 63), (65, 66), (69, 78), (70, 71), (73, 79), (74, 75), (77, 85), (80, 81), (83, 84), (87, 97), (89, 90), (91, 92), (93, 94), (96, 103), (101, 116), (104, 105), (106, 107), (109, 118), (111, 112), (113, 119), (114, 115), (117, 125), (121, 122), (123, 124), (127, 137), (128, 129), (131, 138), (133, 134), (136, 143), (139, 155), (140, 141), (145, 146), (147, 148), (149, 150), (151, 152)]
Layer 2:
[(1, 2), (3, 4), (7, 17), (9, 10), (11, 12), (15, 19), (21, 36), (22, 23), (24, 25), (27, 28), (29, 30), (31, 32), (33, 34), (37, 45), (40, 41), (43, 44), (46, 47), (48, 49), (50, 51), (52, 53), (55, 59), (61, 76), (63, 64), (65, 77), (67, 68), (69, 70), (71, 72), (73, 74), (78, 89), (81, 82), (83, 96), (85, 86), (87, 88), (90, 91), (92, 93), (95, 99), (98, 111), (101, 102), (103, 104), (105, 106), (107, 108), (109, 110), (112, 113), (119, 133), (120, 121), (122, 123), (125, 126), (127, 128), (129, 130), (131, 132), (134, 135), (137, 147), (141, 142), (143, 144), (148, 149), (150, 151), (153, 154)]
הסרת קיוביטים פגומים
בחנו את מפת הקישוריות של ה-Backend וזהו קיוביטים המחוברים לקישורים בעלי שגיאה גבוהה. הסירו קיוביטים "פגומים" אלו מהניסוי.
# Plot gate error map
# NOTE: These can change over time, so your results may look different
plot_error_map(backend)

bad_qubits = {
56,
63,
67,
} # qubits removed based on high coupling error (1.00)
good_qubits = list(set(range(backend.num_qubits)).difference(bad_qubits))
print("Physical qubits:\n", good_qubits)
Physical qubits:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 57, 58, 59, 60, 61, 62, 64, 65, 66, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155]
יצירת Circuit ה-Trotter הראשי
num_steps = 6
theta = Parameter("theta")
circuit = trotter_circuit(
theta, layer_couplings, num_steps, qubits=good_qubits, backend=backend
)
יצירת רשימת ערכי פרמטרים להקצאה מאוחרת
num_params = 12
# 12 parameter values for Rx between [0, pi/2].
# Reshape to outer product broadcast with observables
parameter_values = np.linspace(0, np.pi / 2, num_params).reshape(
(num_params, 1)
)
num_params = parameter_values.size
שלב 2: אופטימיזציה של הבעיה לביצוע על חומרת קוונטום
מעגל ISA
לפני הרצת המעגל על חומרה, יש לבצע אופטימיזציה לביצוע על החומרה. תהליך זה כולל מספר שלבים:
- בחירת פריסת קיוביטים (qubit layout) שממפה את הקיוביטים הווירטואליים של המעגל לקיוביטים פיזיים על החומרה.
- הוספת שערי החלפה (swap gates) לפי הצורך, כדי לנתב אינטראקציות בין קיוביטים שאינם מחוברים.
- תרגום השער ים במעגל להוראות ארכיטקטורת מערכת הוראות (ISA) שניתן להריץ ישירות על החומרה.
- ביצוע אופטימיזציות מעגל כדי למזער את עומק המעגל ואת מספר השערים.
אמנם ה-Transpiler המובנה ב-Qiskit מסוגל לבצע את כל השלבים הללו, אולם מדריך זה מדגים בניית מעגל Trotter בסדר גודל שימוש-מלא מהיסוד. יש לבחור את הקיוביטים הפיזיים הטובים ולהגדיר שכבות שזירה על זוגות קיוביטים מחוברים מתוך הקיוביטים שנבחרו. עם זאת, עדיין יש צורך לתרגם שערים שאינם ISA במעגל ולנצל את כל אופטימיזציות המעגל שמציע ה-Transpiler.
יש לבצע transpile למעגל עבור ה-Backend שנבחר, על ידי יצירת pass manager והרצתו על המעגל. כמו כן, יש לקבע את הפריסה הראשונית של המעגל ל-good_qubits שכבר נבחרו. דרך נוחה ליצור pass manager היא להשתמש בפונקציה generate_preset_pass_manager. עיינו ב-Transpile with pass managers להסבר מפורט יותר על Transpiling עם pass managers.
pm = generate_preset_pass_manager(
backend=backend,
initial_layout=good_qubits,
layout_method="trivial",
optimization_level=1,
)
isa_circuit = pm.run(circuit)
Observable-ים מסוג ISA
כעת, יש ליצור את כל ה-Observable-ים מסוג ממשקל 1 עבור כל קיוביט וירטואלי, על ידי ריפוד של המספר הדרוש של איברי .
observables = []
num_qubits = len(good_qubits)
for q in range(num_qubits):
observables.append(
SparsePauliOp("I" * (num_qubits - q - 1) + "Z" + "I" * q)
)
תהליך ה-Transpilation מיפה את הקיוביטים הווירטואליים של המעגל לקיוביטים פיזיים על החומרה. המידע על פריסת הקיוביטים מאוחסן במאפיין layout של המעגל שעבר transpilation. ה-Observable גם הוא מוגדר במונחים של הקיוביטים הווירטואליים, לכן יש להחיל פריסה זו על ה-Observable. זה נעשה באמצעות מתודת apply_layout של SparsePauliOp.
שימו לב שכל observable עטוף ברשימה בבלוק הקוד הבא. הדבר נעשה כדי לבצע broadcast עם ערכי הפרמטרים, כך שכל observable של קיוביט ייוכד עבור כל ערך theta. כללי ה-broadcasting עבור primitives ניתן למצוא כאן.
isa_observables = [
[obs.apply_layout(layout=isa_circuit.layout)] for obs in observables
]
שלב 3: ביצוע באמצעות Primitives של Qiskit
pub = (isa_circuit, isa_observables, parameter_values)

