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

PEC עם SLC

ביטול שגיאות הסתברותי (PEC) היא טכניקה למיתון רעש קוונטי בחישובי ערכי תצפית של אובזרבבלים. היא מאפשרת לך לחשב הערכה בלתי מוטה של ערכי התצפית. הטכניקה פועלת על ידי הרצת אנסמבל של מעגלים רועשים שמוזרק בהם ההופכי של מודל רעש שנלמד. לאחר מכן מודל הרעש מקורב לערוץ Pauli באמצעות Pauli twirling (שידוע גם בשם randomized compiling). אפשר ללמוד מודלי רעש כאלה ביעילות, ואפשר למתן אותם באמצעות PEC (לפרטים נוספים ראה Probabilistic error cancellation with sparse Pauli-Lindblad models on noisy quantum processors).

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

Shaded Lightcones

המדריך הזה מראה איך להשתמש ב-PEC בשילוב עם SLC כדי למתן רעש בשערים עם תקורת shots מופחתת בהשוואה ל-PEC הרגיל.

זרימת העבודה היא כזו:

  1. מגדירים מעגל ואובזרבבל.
  2. מחשבים ערכי תצפית רועשים ללא מיתון.
  3. מחשבים חסמים קדימה ואחורה על ההטיה.
  4. לומדים את הרעש של שכבות השזירה באמצעות qiskit-noise-learning.
  5. משתמשים ברעש שנלמד כדי למזג את החסמים שחושבו למטריצת קני מידה מקומיים.
  6. בוחרים את חסם ההטיה הרצוי לפי הפשרה בין תקורת shots להטיה.
  7. מכינים תוכנית PEC + SLC, מריצים אותה ומבצעים עיבוד מקדים.
  8. משווים את ערכי התצפית אחרי המיתון ואת תקורת ה-shots.

הגדרת מעגל ואובזרבבל​

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

השתמש בזווית θ=π4\theta= \frac{\pi}{4} כדי להפוך את הדינמיקה ללא-Clifford.

# 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_slc": "qiskit-addon-slc", "qiskit_aer": "qiskit-aer", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "qiskit_mitigation": "qiskit-mitigation", "qiskit_noise_learning": "qiskit-noise-learning", "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 warnings

import numpy as np
from qiskit.circuit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit_mitigation.utils import get_measurement_bases

warnings.filterwarnings("ignore")

def construct_ising_circuit(
num_qubits: int,
num_trotter_steps: int,
rx_angle: float,
) -> QuantumCircuit:
circuit = QuantumCircuit(num_qubits)

for _ 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)

return circuit

def prepare_basis(
circuit: QuantumCircuit, basis: list[int]
) -> QuantumCircuit:
# basis is a list of integer values from 0 to 3. These map to the basis measurement as:
# 0 = I; 1 = Z; 2 = X; 3 = Y
assert len(basis) == circuit.num_qubits

out_circ = circuit.copy_empty_like()
for qb, bas in enumerate(basis):
if bas == 2:
out_circ.h(qb)
elif bas == 3:
out_circ.rx(-np.pi / 2, qb)

out_circ.barrier()
out_circ.compose(circuit, inplace=True)
return out_circ

def mirror_circuit(circuit: QuantumCircuit) -> QuantumCircuit:
mirror_circ = circuit.copy_empty_like()
mirror_circ.compose(circuit.inverse(), inplace=True)
mirror_circ.barrier()
mirror_circ.compose(circuit, inplace=True)
mirror_circ.measure_active()
return mirror_circ

# define observables
num_qubits = 8 # Width of the Ising chain
# The observable to measure: one X component (on qubit 2) and one Z component (on qubit 5)
target_obs_sparse = [("XZ", [2, 5], 1.0)]
observable = SparsePauliOp.from_sparse_list(
target_obs_sparse, num_qubits=num_qubits
)

# Determine the measurement basis each qubit requires for this observable.
bases, _ = get_measurement_bases(observable)

# Instantiate the mirrored circuit and prepend the basis-preparation gates
num_trotter_steps = 6
rx_angle = np.pi / 4
circuit = construct_ising_circuit(num_qubits, num_trotter_steps, rx_angle)
mirrored_circuit = mirror_circuit(circuit)
mirrored_circuit = prepare_basis(mirrored_circuit, bases[0])
print("Visualization of 1 mirrored Trotter step")
prepare_basis(
mirror_circuit(construct_ising_circuit(num_qubits, 1, rx_angle)), bases[0]
).draw("mpl", scale=0.6, fold=-1)
Visualization of 1 mirrored Trotter step

Output of the previous code cell

חישוב ערכי תצפית רועשים ללא מיתון​

QuantumProgram מכילה מעגל קוונטי ו-samplex בפורמט שמתאים לפרימיטיב Executor. MitigationTask.prepare בונה QuantumProgram מהמעגל, מהאובזרבבלים ומערכי הפרמטרים, ועוטף את המעגל בקופסאות (boxes) לפי אפשרויות ה-boxing שניתנו. הגדר את אפשרות ה-boxing ‏measure_annotations ל-"change_basis" כדי לסובב את המדידות לבסיסים הנדרשים, והוסף add_tags: "unique_box" כדי שהסימולטור יוכל למפות מודל רעש על הקופסאות של המעגל. התוכנית עוברת Pauli twirling כברירת מחדל, מה שבחומרה הופך רעש קוהרנטי לרעש Pauli שאליו שיטות המיתון מניחות. num_randomizations קובע כמה מופעים מסובבים של כל מעגל נדגמים, ו-shots_per_randomization קובע כמה shots מקבל כל מופע, כך שהמכפלה שלהם היא מגבלת ה-shots לכל מעגל.

אם יש לך גישה ל-QPU, אפשר להריץ את התוכנית באמצעות Executor שנבנה מ-Backend של QiskitRuntimeService. הדוגמה הזו משתמשת ב-AerSimulator כדי להריץ את התוכנית באופן מקומי. אפשרות הסימולטור layer_noise_model שלו מזריקה רעש ממודל רעש Pauli-Lindblad נתון בכל שכבה של שערי שזירה. כאן אנחנו מייצרים מודל רעש Pauli-Lindblad אקראי עם שגיאה לכל שער בשכבה (EPLG) של 0.01, כמעט סדר גודל מעל ה-QPU-ים הנוכחיים האופייניים מסוג Heron, כדי להגדיל את ההשפעה של השימוש ב-SLC גם בבעיה בקנה מידה קטן.

from qiskit.circuit import CircuitInstruction
from qiskit.quantum_info import PauliLindbladMap, pauli_basis
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import Executor
from qiskit_mitigation import MitigationTask, find_combined_unique_layers
from samplomatic import Tag
from samplomatic.utils import get_annotation

def generate_random_pauli_lindblad_noise(
layers,
num_qubits: int,
eplg: float = 3e-3,
seed: int | None = None,
) -> list[tuple[CircuitInstruction, PauliLindbladMap]]:
"""Generate random Pauli-Lindblad noise over the full Pauli basis."""
layer_noise_model = []
for layer in layers:
layer_instructions = layer.operation.params[0]
contain_measurement = any(
inst.name.startswith("measure") for inst in layer_instructions
)
if contain_measurement: # do not add measurement noise
layer_noise = PauliLindbladMap.identity(num_qubits)
else:
edges = [
(inst.qubits[0]._index, inst.qubits[1]._index)
for inst in layer_instructions
] # extract the edges of the layer

basis_paulis = [p for p in pauli_basis(2) if np.sum(p.x + p.z)]
rng = np.random.default_rng(seed=seed)
num_generators = len(basis_paulis)
rates = rng.random(len(basis_paulis)) * eplg / num_generators

layer_noise = PauliLindbladMap.from_sparse_list(
[
(pauli.to_label(), edge, rate)
for pauli, rate in zip(basis_paulis, rates, strict=True)
for edge in edges
],
num_qubits=num_qubits,
)
layer_noise_model.append((layer, layer_noise))
return layer_noise_model

num_randomizations = 400
shots_per_randomization = 16

boxing_options = {"add_tags": "unique_box"}

# find the unique layers to generate the layered noise for
box_layers = find_combined_unique_layers(
[mirrored_circuit], custom_boxing_options=boxing_options
)
layer_noise_model = generate_random_pauli_lindblad_noise(
box_layers, num_qubits, eplg=1e-2, seed=42
)

# a local noisy simulator in place of a QPU backend
noisy_executor = Executor(
AerSimulator(),
options={"simulator": {"layer_noise_model": layer_noise_model}},
)

task = MitigationTask()
program = task.prepare(
circuit=mirrored_circuit,
observables=observable,
parameters=None,
num_randomizations=num_randomizations,
shots_per_randomization=shots_per_randomization,
custom_boxing_options=boxing_options,
)

program._semantic_role = "simulation" # flag the program for the simulator
job_unmit_res = noisy_executor.run(program).result()
unmitigated_res = task.postprocess(job_unmit_res)

print(f"unmitigated result: {unmitigated_res.data.evs[0]}")
unmitigated result: 0.63125

חישוב חסמים קדימה ואחורה על ההטיה​

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

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

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

שני החסמים יחד יוצרים סוגריים הדוקים סביב התרומה במקרה הגרוע של כל שגיאה להטיה. בשלב הבא החסמים האלה ממוזגים עם שיעורי הרעש שנלמדו כדי לדרג אילו שגיאות כדאי לתקן ואילו אפשר להשאיר בלי חשש. אפשרות ה-boxing ‏inject_noise_strategy="individual_modification" נדרשת כאן כדי שלכל annotation מסוג InjectNoise במעגל העטוף בקופסאות תהיה זהות ייחודית שאליה אפשר לצרף את חסמי ה-SLC.

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

from qiskit_addon_slc.bounds import (
compute_backward_bounds,
compute_forward_bounds,
tighten_with_speed_limit,
)
from qiskit_mitigation import PEC

slc_boxing_options = {
"add_tags": "unique_box",
"inject_noise_strategy": "individual_modification",
}

slc_pec_task = PEC()

boxed_circuit, noise_model_paulis = (
slc_pec_task.generate_slc_bounds_computation_input(
mirrored_circuit, custom_boxing_options=slc_boxing_options
)
)

forward_bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable
)
tighten_forward_bounds = tighten_with_speed_limit(
forward_bounds, boxed_circuit, noise_model_paulis, observable
)
backward_bounds = compute_backward_bounds(boxed_circuit, noise_model_paulis)

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

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

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

qiskit-noise-learning משמש ללימוד הרעש. הלומד חוזר על כל שכבה fragment_depths פעמים במעגלי הלמידה שלו, מסובב (twirl) כל מעגל num_randomizations פעמים, ומתאים את דעיכת הנאמנויות (fidelities) הנמדדות למודל Pauli-Lindblad. הפרמטרים האלה יכולים להשפיע על הדיוק של המודל שנלמד. ככל שמודל הרעש שנלמד מדויק יותר, כך ביטול הרעש יהיה מדויק יותר.

הקוד הבא משרטט את מודל הרעש שנלמד, כששתי השכבות ממוזגות למפה אחת. כל Qubit הוא עוגה של שיעורי X, Y ו-Z ממשקל 1 שסוכמו על פני שתי השכבות, וכל צלע מראה את השיעורים ממשקל 2 שנלמדו. למשל, שיעור של 3e-4 אומר שהשגיאה מתרחשת בערך שלוש פעמים מכל 10,000 שימושים בשכבה.

from qiskit.providers.fake_provider import GenericBackendV2
from qiskit.quantum_info import PauliList
from qiskit_ibm_runtime.results import LayerError, PauliLindbladError
from qiskit_ibm_runtime.visualization.embeddings import Embedding
from qiskit_noise_learning.aer_executor import AerExecutor
from qiskit_noise_learning.noise_learner import LearningOptions, NoiseLearner
from samplomatic import InjectNoise

line_coupling = [(i, i + 1) for i in range(num_qubits - 1)]
backend = GenericBackendV2(
num_qubits=num_qubits,
basis_gates=["id", "rz", "sx", "x", "cz"],
coupling_map=line_coupling,
)

# first learn only the layers PEC will inject noise into, i.e. not the measurement layer
pec_layers = [
layer
for layer in slc_pec_task.find_unique_layers(
mirrored_circuit, slc_boxing_options
)
if get_annotation(layer.operation, InjectNoise)
]
layers = slc_pec_task.find_unique_layers(mirrored_circuit, slc_boxing_options)

learning_options = LearningOptions(
fragment_depths=[2, 4, 8, 16],
num_randomizations=250,
shots_per_randomizations=16,
)

# qiskit-noise-learning is not compatible with the Executor in local mode,
# and requires the simulator present in the package for running simulation
noise_dict = {
get_annotation(layer.operation, Tag).ref: noise_layer
for layer, noise_layer in layer_noise_model
}
noise_learner_simulator = AerExecutor(
qasm_simulator=AerSimulator(), noise_dict=noise_dict
)
learner = NoiseLearner(
backend, options=learning_options, executor=noise_learner_simulator
)
learned_noise = (
learner.run(pec_layers).result().to_dict()
) # keyed by InjectNoise reference

# Draw the learned model on the line, laid out as a line with the qubits in order.
# Composing the two layers' maps sums their rates, giving one map over all the edges.
embedding = Embedding(
[(0, 0), (0, 2), (0, 4), (2, 4), (4, 4), (4, 2), (4, 0), (2, 0)],
line_coupling,
)
first, second = learned_noise.values()
learned_model = first.compose(second).simplify()
error = PauliLindbladError(
PauliList(
learned_model.get_qubit_sparse_pauli_list_copy().to_pauli_list()
),
learned_model.rates,
)
fig = LayerError(circuit, list(range(num_qubits)), error).draw_map(
embedding, radius=0.4, height=460, width=620
)
fig.update_traces(
marker={
"cmin": 0,
"cmax": max(learned_model.rates),
"colorbar": {"tickformat": ".1e", "title": "rate"},
},
selector={"mode": "markers"},
)
fig.show()

Output of the previous code cell

שימוש ברעש שנלמד כדי למזג את החסמים שחושבו למטריצת קני מידה מקומיים​

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

הרעיון המרכזי של SLC הוא שלא צריך לבטל לגמרי את כל השגיאות. לשגיאה ששני החסמים שלה, קדימה ואחורה, קטנים, יש השפעה זניחה על האובזרבבל בלי קשר לשיעור שלה, ולכן אפשר להשאיר אותה ללא מיתון חלקית או לגמרי. merge_bounds ממזג את החסמים הקלאסיים עם שיעורי Pauli-Lindblad שנלמדו, ואז compute_local_scales מקצה לכל שגיאה קנה מידה בין 0 (מתעלמים לגמרי) ל-1 (מבטלים לגמרי), בכפוף ל-bias_tolerance שהמשתמש מציין. שגיאות עם תרומת הטיה משוערת גדולה מקבלות קנה מידה קרוב ל-1 (טיפול PEC מלא), ושגיאות עם תרומה קטנה מקבלות קנה מידה נמוך יותר, מה שמפחית את תקורת הדגימה שהן נושאות.

SLC חושף כפתור רציף בין שני הקצוות — PEC רגיל (אפס הטיה, התקורה הגבוהה ביותר) ובלי מיתון בכלל (אפס תקורה, הטיית רעש מלאה). הפרמטר bias_tolerance שולט בכפתור הזה: הגדרתו ל-0 מחזירה PEC מלא, בעוד שערכים גדולים יותר מאפשרים יותר הטיה שיורית בתמורה לתקורת דגימה נמוכה יותר γ².

השלב הזה סורק את bias_tolerance על פני טווח ערכים ומחשב את תקורת הדגימה שמתקבלת לכל אחד מהם. שרטוט הנקודות האלה מול התקורה של PEC מלא עוזר לך לבחור נקודת עבודה שמתאימה לתקציב ה-shots שלך. למשל, סבילות להטיה שיורית של 5% עשויה להפחית את תקורת הדגימה בסדר גודל לעומת PEC מלא, ובכך להפוך את החישוב לאפשרי ב-QPU-ים של היום, שבלעדיה לא היה אפשרי.

import matplotlib.pyplot as plt
from qiskit_addon_slc.bounds import compute_local_scales, merge_bounds

merged_bounds = merge_bounds(
boxed_circuit,
tighten_forward_bounds,
backward_bounds,
noise_rates=learned_noise,
)

biases = []
costs = []
for bias in [0.0, *np.arange(0.01, 0.11, 0.01).tolist()]:
_, cost_, bias_ = compute_local_scales(
boxed_circuit,
merged_bounds,
noise_rates=learned_noise,
bias_tolerance=bias,
)
biases.append(bias_)
costs.append(cost_)

# Compute tha full PEC overhead and compare it with the overhead and bias of different SLC configurations
vanilla_gamma = slc_pec_task.calculate_gamma(
slc_pec_task.boxed_circuit, learned_noise, 1
)

xticks = np.arange(0, 11, 2)

fig, ax = plt.subplots()
ax.scatter(
[0], [vanilla_gamma**2], marker="D", c="tab:orange", label="full PEC"
)
ax.plot(
100 * np.array(biases),
np.array(costs),
"o-",
c="tab:blue",
label="PEC+SLC",
)
ax.set_xticks(xticks, [f"{x:.2f}" for x in xticks])

ax.set_xlabel("Remaining bias [%]")
ax.set_ylabel(r"Sampling overhead, $\gamma^2$")
ax.grid()
ax.legend()
fig.suptitle("PEC sampling overhead reduction due to SLC")
Optimal spacetime partitioning not implemented! Just partitioning list of noisy boxes.
Text(0.5, 0.98, 'PEC sampling overhead reduction due to SLC')

Output of the previous code cell

בחירת חסם ההטיה הרצוי לפי הפשרה בין תקורת shots להטיה​

במדריך הזה אנחנו בוחרים סבילות הטיה של 5%. שים לב שחישוב ההטיה הזה מבוסס על מודל הרעש שנלמד, שאולי לא מתיישר בצורה מושלמת עם הרעש האמיתי של המכשיר.

chosen_bias_threshold = 0.05
local_scales, sampling_cost, bias = compute_local_scales(
boxed_circuit, merged_bounds, learned_noise, bias_tolerance=bias
)
slc_pec_task.set_slc_local_scales(
local_scales, float(np.sqrt(sampling_cost)), bias
)

הכנת תוכנית PEC + SLC, הרצתה ועיבוד מקדים​

אחרי שבחרנו את קני המידה המקומיים, אפשר לבנות את תוכנית ההרצה המלאה של PEC+SLC. הקריאה PEC.prepare בונה QuantumProgram שמקודד את אסטרטגיית הזרקת האנטי-רעש ל-annotations של הקופסאות במעגל. העברת noise_gain="slc_local_scales" מורה ל-executor להשתמש במשקלים לכל שגיאה שנקבעו על ידי set_slc_local_scales, במקום להחיל את אותו גאין מלא של רעש הפוך על כל שגיאה, כפי ש-PEC רגיל היה עושה.

כדי ליצור השוואה הוגנת, מכינים משימת PEC שנייה עם noise_gain=0, שמתאימה לביטול PEC מלא בלי מרווח הטיה של SLC. שתי התוכניות מתווספות לאותה QuantumProgram כך שהן חולקות job הרצה אחד — המעגלים שלהן משולבים ורצים יחד, כלומר כל סחיפת חומרה משפיעה על שתיהן באופן שווה.

אחרי שה-job מסתיים, postprocess ממיר את מחרוזות הביטים הגולמיות של המדידה לערכי תצפית, ומחיל את גורם ההסקלה המתאים של PEC‏ γ לכל משימה.

program_pec = slc_pec_task.prepare(
circuit=mirrored_circuit,
observables=[observable],
parameters=None,
num_randomizations=num_randomizations,
shots_per_randomization=shots_per_randomization,
custom_boxing_options=slc_boxing_options,
noise_maps=learned_noise, # PEC option
noise_gain="slc_local_scales", # PEC option. Defines the usage of SLC
max_sampling_overhead=100, # PEC option
)

full_pec_task = PEC()
program_pec = full_pec_task.prepare(
circuit=mirrored_circuit,
observables=[observable],
parameters=None,
num_randomizations=num_randomizations,
shots_per_randomization=shots_per_randomization,
custom_boxing_options=boxing_options,
quantum_program=program_pec,
noise_maps=learned_noise, # PEC option
noise_gain=0, # PEC option
max_sampling_overhead=100, # PEC option
)

# Execute
program_pec._semantic_role = (
"simulation" # flag the program for the simulator
)
job_pec_results = noisy_executor.run(program_pec).result()

# post-processing
pec_slc_res = slc_pec_task.postprocess(job_pec_results)
full_pec_res = full_pec_task.postprocess(job_pec_results)

השוואת ערכי התצפית אחרי המיתון ותקורת ה-shots​

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

  • ללא מיתון: ערך התצפית הרועש הגולמי בלי ביטול שגיאות כלשהו.

  • PEC מלא: ביטול שגיאות ללא הטיה באמצעות מודל הרעש ההופכי המלא בכל שכבה, בתקורת הדגימה המלאה γ².

  • PEC+SLC: ביטול שגיאות בהנחיית קני המידה המקומיים של shaded-lightcone, תוך סבילות ל-bias_tolerance שנבחר, בתמורה למספר shots נמוך משמעותית.

השוואת שלוש השורות מראה את התועלת המעשית של SLC: ההטיה אחרי מיתון של PEC+SLC נשארת הרבה מתחת להטיה ללא מיתון, בעוד שהמספר הכולל של ה-shots קטן משמעותית ממה ש-PEC מלא דרש.

print(
f"Unmitigated result bias: {np.abs(1 - unmitigated_res.data.evs[0]) * 100:.2f}%, \
total shots ran: {job_unmit_res[0]['_meas'].shape[0] * job_unmit_res[0]['_meas'].shape[2]}"
)

print(
f"Full PEC final result bias: {np.abs(1 - full_pec_res.data.evs[0]) * 100:.2f}%, \
total shots ran: {job_pec_results[1]['_meas'].shape[0] * job_pec_results[1]['_meas'].shape[2]}"
)

print(
f"PEC+SLC final result bias: {np.abs(1 - pec_slc_res.data.evs[0]) * 100:.2f}%, \
total shots ran: {job_pec_results[0]['_meas'].shape[0] * job_pec_results[0]['_meas'].shape[2]}"
)
Unmitigated result bias: 36.88%, total shots ran: 6400
Full PEC final result bias: 0.97%, total shots ran: 48528
PEC+SLC final result bias: 1.46%, total shots ran: 24480