PEC cu SLC
Anularea probabilistică a erorilor (PEC) este o tehnică de atenuare a zgomotului cuantic în calculul valorilor de așteptare ale observabilelor. Îți permite să calculezi o estimare nepărtinitoare a valorilor de așteptare. Tehnica funcționează executând un ansamblu de circuite cu zgomot în care este injectat inversul unui model de zgomot învățat. Modelul de zgomot este apoi aproximat la un canal Pauli prin Pauli twirling (cunoscut și ca randomized compiling). Astfel de modele de zgomot pot fi învățate eficient și pot fi mitigate folosind PEC (vezi Probabilistic error cancellation with sparse Pauli-Lindblad models on noisy quantum processors pentru mai multe detalii).
PEC necesită un overhead exponențial de shot-uri pentru a calcula o estimare nepărtinitoare. Shaded Lightcones (SLC) este o tehnică ce folosește resurse clasice pentru a limita sensibilitatea la erori în întreg circuitul. Folosind aceste limite, utilizatorul poate ajusta controlat părtinirea în estimarea observabilei.
Acest ghid arată cum să folosești PEC combinat cu SLC pentru a atenua zgomotul porților cu un overhead de shot-uri redus față de PEC simplu.
Fluxul de lucru este următorul:
- Specifică un circuit și o observabilă.
- Calculează valorile de așteptare cu zgomot, nemitigate.
- Calculează limitele înainte și înapoi ale părtinirii.
- Învață zgomotul straturilor de încurcare folosind qiskit-noise-learning.
- Folosește zgomotul învățat pentru a îmbina limitele calculate într-o matrice de scale locale.
- Alege limita dorită a părtinirii pe baza compromisului dintre overhead-ul de shot-uri și părtinire.
- Pregătește un program PEC + SLC, execută programul și efectuează post-procesarea.
- Compară valorile de așteptare mitigate și overhead-ul de shot-uri.
Specifică un circuit și o observabilă
Pregătește un circuit oglindă al unei evoluții în timp Trotterizate a unui hamiltonian Ising 1D pe opt qubiți. Oglindește circuitul pentru a anula unitarul său și a forma un circuit identitate, astfel încât valoarea de așteptare ideală a fiecărei observabile să fie cunoscută ca 1, fără a fi nevoie s-o calculezi. În plus, oglindirea dublează adâncimea circuitului, ceea ce dublează la rândul său efectul zgomotului.
Folosește unghiul pentru a face dinamica non-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
Calculează valorile de așteptare cu zgomot, nemitigate
Un QuantumProgram conține un circuit cuantic și un samplex într-un format potrivit pentru primitiva Executor. MitigationTask.prepare construiește un QuantumProgram din circuit, observabile și valorile parametrilor și încadrează circuitul în casete (boxing) cu opțiunile de boxing date. Setează opțiunea de boxing measure_annotations la "change_basis" pentru a roti măsurătorile în bazele necesare și adaugă add_tags: "unique_box" pentru ca simulatorul să poată asocia un model de zgomot casetelor circuitului. Programul este implicit supus Pauli twirling, care pe hardware transformă zgomotul coerent în zgomotul Pauli presupus de metodele de mitigare. num_randomizations stabilește câte instanțe twirled ale fiecărui circuit sunt eșantionate, iar shots_per_randomization stabilește câte shot-uri primește fiecare instanță, astfel că produsul lor este limita de shot-uri per circuit.
Dacă ai acces la QPU, poți executa programul cu un Executor construit dintr-un backend al QiskitRuntimeService. Acest exemplu folosește un AerSimulator pentru a rula programul local. Opțiunea sa de simulator layer_noise_model injectează zgomot dintr-un model de zgomot Pauli-Lindblad dat pe fiecare strat de porți de încurcare. Aici generăm un model de zgomot Pauli-Lindblad aleatoriu cu o eroare per poartă în strat (EPLG) de 0.01, aproape cu un ordin de mărime mai mare decât QPU-urile Heron actuale tipice, pentru a amplifica efectul utilizării SLC chiar și pe o problemă de scară mică.
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
Calculează limitele înainte și înapoi ale părtinirii
Înainte de a consuma timp QPU pentru învățarea zgomotului, SLC folosește simularea clasică pentru a limita contribuția fiecărei erori potențiale la părtinirea observabilei. Aceasta se face în două treceri complementare:
-
Propagare înainte: fiecare eroare Pauli este propagată înainte prin restul circuitului și se evaluează comutatorul ei cu observabila. O eroare care anticomută cu observabila este singurul tip care poate părtini rezultatul, așa că această trecere produce o limită superioară a impactului înainte al fiecărei erori.
-
Propagare înapoi: fiecare eroare este propagată înapoi până la începutul circuitului și se evaluează comutatorul ei cu starea inițială. Deoarece circuitul pornește din starea cu toți qubiții în zero, erorile care comută cu ea nu pot afecta rezultatul, strângând limita din cealaltă direcție.
Cele două limite împreună formează un interval strâns în jurul contribuției celei mai defavorabile a fiecărei erori la părtinire. În pasul următor, aceste limite sunt îmbinate cu ratele de zgomot învățate pentru a stabili care erori merită corectate și care pot fi lăsate în siguranță neatinse. Opțiunea de boxing inject_noise_strategy="individual_modification" este necesară aici pentru ca fiecare adnotare InjectNoise din circuitul încadrat în casete să aibă o identitate unică de care pot fi atașate limitele SLC.
Pentru probleme de scară mai mare, aceste calcule pot necesita resurse de calcul clasic substanțiale.
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)
Învață zgomotul straturilor de încurcare
Pentru a calcula o estimare finală a cât de mult influențează fiecare eroare părtinirea valorii de așteptare, precum și cât de costisitor ar fi s-o corectăm, învățăm ratele de zgomot și le aplicăm limitelor de zgomot prezise pe care le-am calculat înainte.
PEC (precum și PEC+SLC) presupune un model de zgomot twirled al fiecărui strat de porți de încurcare din circuit. Dacă presupui un model de zgomot markovian, se presupune că adâncimea circuitului nu afectează canalul de zgomot al unui strat de încurcare dat. Prin urmare, trebuie învățat doar modelul de zgomot al fiecărui strat unic de porți de încurcare. Circuitul folosit în acest ghid poate fi împărțit în două straturi unice. Proiectarea circuitelor astfel încât să aibă un set minim de straturi de încurcare unice este importantă dacă vrei să înveți un model de zgomot, deoarece straturile unice trebuie învățate independent. Dacă ai prea multe straturi unice în circuit, învățarea zgomotului poate deveni imposibilă din cauza costului de eșantionare al învățării mai multor straturi.
qiskit-noise-learning este folosit pentru a învăța zgomotul. Learner-ul repetă fiecare strat de fragment_depths ori în circuitele sale de învățare, aplică twirling fiecărui circuit de num_randomizations ori și potrivește descreșterea fidelităților măsurate la un model Pauli-Lindblad. Acești parametri pot afecta acuratețea modelului învățat. Cu cât modelul de zgomot învățat este mai precis, cu atât anularea zgomotului va fi mai precisă.
Codul următor desenează modelul de zgomot învățat, cu cele două straturi îmbinate într-o singură hartă. Fiecare qubit este o diagramă circulară a ratelor sale X, Y și Z de pondere 1, însumate peste cele două straturi, iar fiecare muchie arată ratele de pondere 2 învățate. De exemplu, o rată de 3e-4 înseamnă că eroarea apare aproximativ la trei din fiecare 10.000 de utilizări ale stratului.
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()
Folosește zgomotul învățat pentru a îmbina limitele calculate într-o matrice de scale locale
Având atât limitele clasice (din pasul anterior), cât și ratele de zgomot învățate, putem calcula acum o matrice de scale locale — o pondere per eroare care determină cât de agresiv ar trebui PEC să anuleze fiecare termen de zgomot în parte.
Ideea cheie a SLC este că nu toate erorile trebuie anulate complet. O eroare ale cărei limite înainte și înapoi sunt amândouă mici are un efect neglijabil asupra observabilei indiferent de rata ei, deci poate fi lăsată parțial sau complet nemitigată. merge_bounds îmbină limitele clasice cu ratele Pauli-Lindblad învățate, apoi compute_local_scales atribuie fiecărei erori o scală între 0 (ignorată complet) și 1 (anulată complet), sub rezerva unui bias_tolerance specificat de utilizator. Erorile cu o contribuție estimată mare la părtinire primesc o scală aproape de 1 (tratament PEC complet), în timp ce erorile cu o contribuție mică primesc o scală mai mică, reducând overhead-ul de eșantionare pe care îl implică.
SLC oferă un buton continuu între cele două extreme — PEC simplu (părtinire zero, overhead maxim) și nicio mitigare (overhead zero, părtinire completă din zgomot). Parametrul bias_tolerance controlează acest buton: setarea lui la 0 redă PEC complet, în timp ce valori mai mari permit mai multă părtinire reziduală în schimbul unui overhead de eșantionare γ² mai mic.
Acest pas parcurge bias_tolerance pe un interval de valori și calculează overhead-ul de eșantionare rezultat pentru fiecare. Reprezentarea grafică a acestor puncte față de overhead-ul PEC complet te ajută să alegi un punct de lucru care se potrivește bugetului tău de shot-uri. De exemplu, tolerarea unei părtiniri reziduale de 5 % poate reduce overhead-ul de eșantionare cu un ordin de mărime față de PEC complet, făcând calculul fezabil pe QPU-urile de azi, unde altfel nu ar fi.
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')
Alege limita dorită a părtinirii pe baza compromisului dintre overhead-ul de shot-uri și părtinire
În acest ghid alegem o toleranță a părtinirii de 5%. Reține că acest calcul al părtinirii se bazează pe modelul de zgomot învățat, care ar putea să nu fie perfect aliniat cu zgomotul real al dispozitivului.
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
)
Pregătește un program PEC + SLC, execută programul și efectuează post-procesarea
Cu scalele locale alese, putem construi programul complet de execuție PEC+SLC. Apelul PEC.prepare construiește un QuantumProgram care codifică strategia de injectare a anti-zgomotului în adnotările casetelor circuitului. Transmiterea noise_gain="slc_local_scales" instruiește executorul să folosească ponderile per eroare stabilite de set_slc_local_scales, în loc să aplice același câștig complet de zgomot invers fiecărei erori, cum ar face PEC simplu.
Pentru a produce o comparație corectă, se pregătește un al doilea task PEC cu noise_gain=0, care corespunde anulării PEC complete, fără toleranță de părtinire SLC. Ambele programe sunt adăugate aceluiași QuantumProgram, astfel încât să partajeze un singur job de execuție — circuitele lor sunt intercalate și executate împreună, ceea ce înseamnă că orice derivă a hardware-ului le afectează în mod egal.
După terminarea job-ului, postprocess convertește șirurile de biți brute ale măsurătorilor în valori de așteptare, aplicând factorul de rescalare PEC γ corespunzător fiecărui task.
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)
Compară valorile de așteptare mitigate și overhead-ul de shot-uri
Valoarea de așteptare ideală a circuitului oglindă este 1 prin construcție, deci părtinirea oricărei estimări este pur și simplu |1 - result|. Această celulă finală afișează părtinirea și numărul total de shot-uri pentru trei scenarii, unul lângă altul:
-
Nemitigat: valoarea de așteptare brută cu zgomot, fără nicio anulare a erorilor.
-
PEC complet: anulare a erorilor fără părtinire, folosind modelul complet de zgomot invers pe fiecare strat, la overhead-ul de eșantionare γ² complet.
-
PEC+SLC: anulare a erorilor ghidată de scalele locale shaded-lightcone, tolerând
bias_toleranceales în schimbul unui număr substanțial mai mic de shot-uri.
Compararea celor trei rânduri arată beneficiul concret al SLC: părtinirea mitigată a PEC+SLC rămâne mult sub părtinirea nemitigată, în timp ce numărul total de shot-uri este semnificativ mai mic decât cel cerut de PEC complet.
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