Vai al contenuto principale
Questo progetto è in fase beta. Hai trovato un bug? Apri una issue su GitHub. Hai una domanda o un'idea? Chiedi su GitHub Discussions — apprezziamo il tuo feedback!
Com'è la qualità della traduzione su questa pagina?

PEC con SLC

La cancellazione probabilistica degli errori (PEC) è una tecnica per mitigare il rumore quantistico nei calcoli dei valori di aspettazione di osservabili. Ti permette di calcolare una stima non distorta dei valori di aspettazione. Questa tecnica funziona eseguendo un insieme di circuiti rumorosi in cui viene iniettato l'inverso di un modello di rumore appreso. Il modello di rumore viene poi approssimato a un canale di Pauli tramite il Pauli twirling (noto anche come randomized compiling). Tali modelli di rumore possono essere appresi in modo efficiente e possono essere mitigati con la PEC (vedi Probabilistic error cancellation with sparse Pauli-Lindblad models on noisy quantum processors per maggiori dettagli).

La PEC richiede un overhead di shot esponenziale per calcolare una stima non distorta. Shaded Lightcones (SLC) è una tecnica che usa risorse classiche per limitare la sensibilità agli errori in tutto il circuito. Usando questi limiti, l'utente può regolare in modo controllato il bias nella stima dell'osservabile.

Shaded Lightcones

Questa guida mostra come usare la PEC combinata con SLC per mitigare il rumore dei gate con un overhead di shot ridotto rispetto alla PEC standard.

Il flusso di lavoro è il seguente:

  1. Specifica un circuito e un'osservabile.
  2. Calcola i valori di aspettazione rumorosi e non mitigati.
  3. Calcola i limiti forward e backward sul bias.
  4. Apprendi il rumore dei layer di entanglement usando qiskit-noise-learning.
  5. Usa il rumore appreso per unire i limiti calcolati in una matrice di scale locali.
  6. Scegli il limite di bias desiderato in base al compromesso tra overhead di shot e bias.
  7. Prepara un programma PEC + SLC, eseguilo ed effettua il post-processing.
  8. Confronta i valori di aspettazione mitigati e l'overhead di shot.

Specifica un circuito e un'osservabile​

Prepara un circuito speculare di un'evoluzione temporale trotterizzata dell'hamiltoniana di una catena di Ising 1D su otto qubit. Rispecchia il circuito per annullarne l'unitaria e formare un circuito identità, così che il valore di aspettazione ideale di ciascuna osservabile sia noto e pari a 1 senza bisogno di calcolarlo. Inoltre, lo specchio raddoppia la profondità del circuito, il che a sua volta raddoppia l'effetto del rumore.

Usa l'angolo θ=π4\theta= \frac{\pi}{4} per rendere la 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

Output of the previous code cell

Calcola i valori di aspettazione rumorosi e non mitigati​

Un QuantumProgram contiene un circuito quantistico e un samplex in un formato adatto alla primitiva Executor. MitigationTask.prepare costruisce un QuantumProgram a partire dal circuito, dalle osservabili e dai valori dei parametri, e racchiude il circuito in box con le opzioni di boxing indicate. Imposta l'opzione di boxing measure_annotations su "change_basis" per ruotare le misure nelle basi richieste, e aggiungi add_tags: "unique_box" in modo che il simulatore possa associare un modello di rumore ai box del circuito. Il programma viene sottoposto a Pauli twirling per impostazione predefinita, che su hardware trasforma il rumore coerente nel rumore di Pauli assunto dai metodi di mitigazione. num_randomizations imposta quante istanze twirled di ciascun circuito vengono campionate e shots_per_randomization imposta quanti shot riceve ciascuna istanza, quindi il loro prodotto è il limite di shot per circuito.

Se hai accesso a una QPU, puoi eseguire il programma con un Executor costruito a partire da un backend di QiskitRuntimeService. Questo esempio usa un AerSimulator per eseguire il programma in locale. La sua opzione di simulatore layer_noise_model inietta il rumore di un dato modello di Pauli-Lindblad su ogni layer di gate di entanglement. Qui generiamo un modello di rumore di Pauli-Lindblad casuale con un errore per gate a livello di layer (EPLG) di 0.01, quasi un ordine di grandezza superiore alle tipiche QPU Heron attuali, per amplificare l'effetto dell'uso di SLC anche su un problema di piccola scala.

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

Calcola i limiti forward e backward sul bias​

Prima di spendere tempo QPU sull'apprendimento del rumore, SLC usa la simulazione classica per limitare il contributo di ciascun potenziale errore al bias dell'osservabile. Questo avviene in due passaggi complementari:

  • Propagazione forward: ogni errore di Pauli viene propagato in avanti attraverso il resto del circuito e se ne valuta il commutatore con l'osservabile. Un errore che anticommuta con l'osservabile è l'unico tipo che può distorcere il risultato, quindi questo passaggio produce un limite superiore sull'impatto forward di ciascun errore.

  • Propagazione backward: ogni errore viene propagato all'indietro fino all'inizio del circuito e se ne valuta il commutatore con lo stato iniziale. Poiché il circuito parte dallo stato di tutti zeri, gli errori che commutano con esso non possono influenzare il risultato, restringendo il limite dall'altra direzione.

I due limiti insieme formano un intervallo stretto attorno al contributo peggiore di ogni errore al bias. Nel passaggio successivo questi limiti vengono uniti ai tassi di rumore appresi per classificare quali errori vale la pena correggere e quali possono essere lasciati tranquillamente da parte. L'opzione di boxing inject_noise_strategy="individual_modification" è richiesta qui affinché ogni annotazione InjectNoise nel circuito racchiuso in box abbia un'identità univoca a cui associare i limiti SLC.

Per problemi su scala più ampia, questi calcoli possono richiedere notevoli risorse computazionali classiche.

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)

Apprendi il rumore dei layer di entanglement​

Per calcolare una stima finale di quanto ciascun errore incida sul bias del valore di aspettazione, e di quanto sarebbe costoso correggerlo, apprendiamo i tassi di rumore e li applichiamo ai limiti di rumore previsti che abbiamo calcolato prima.

La PEC (così come PEC+SLC) assume un modello di rumore twirled di ciascun layer di gate di entanglement nel circuito. Se assumi un modello di rumore markoviano, si assume che la profondità del circuito non influenzi il canale di rumore di un dato layer di entanglement. Pertanto, occorre apprendere solo il modello di rumore di ciascun layer univoco di gate di entanglement. Il circuito usato in questa guida può essere suddiviso in due layer univoci. Progettare circuiti con un insieme minimo di layer di entanglement univoci è importante se vuoi apprendere un modello di rumore, poiché i layer univoci devono essere appresi in modo indipendente. Se nel circuito ci sono troppi layer univoci, l'apprendimento del rumore può diventare impraticabile a causa del costo di campionamento dell'apprendimento di molti layer.

qiskit-noise-learning viene usato per apprendere il rumore. Il learner ripete ogni layer fragment_depths volte nei suoi circuiti di apprendimento, applica il twirling a ciascun circuito num_randomizations volte e adatta il decadimento delle fedeltà misurate a un modello di Pauli-Lindblad. Questi parametri possono influire sull'accuratezza del modello appreso. Più accurato è il modello di rumore appreso, più accurata sarà la cancellazione del rumore.

Il codice seguente disegna il modello di rumore appreso con i due layer uniti in un'unica mappa. Ogni qubit è un grafico a torta dei suoi tassi X, Y e Z di peso 1 sommati sui due layer, e ogni arco mostra i tassi di peso 2 appresi. Ad esempio, un tasso di 3e-4 significa che l'errore si verifica circa tre volte ogni 10.000 utilizzi del layer.

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

Usa il rumore appreso per unire i limiti calcolati in una matrice di scale locali​

Con sia i limiti classici (dal passaggio precedente) sia i tassi di rumore appresi a disposizione, possiamo ora calcolare una matrice di scale locali, un peso per errore che determina quanto aggressivamente la PEC debba cancellare ciascun singolo termine di rumore.

L'idea chiave di SLC è che non tutti gli errori devono essere cancellati completamente. Un errore i cui limiti forward e backward sono entrambi piccoli ha un effetto trascurabile sull'osservabile indipendentemente dal suo tasso, quindi può essere lasciato parzialmente o completamente non mitigato. merge_bounds unisce i limiti classici con i tassi di Pauli-Lindblad appresi, poi compute_local_scales assegna a ciascun errore una scala tra 0 (completamente ignorato) e 1 (completamente cancellato), soggetta a una bias_tolerance specificata dall'utente. Gli errori con un grande contributo stimato al bias ricevono una scala vicina a 1 (trattamento PEC completo), mentre quelli con un contributo piccolo ricevono una scala inferiore, riducendo l'overhead di campionamento che comportano.

SLC offre una manopola continua tra i due estremi: la PEC standard (bias zero, overhead massimo) e nessuna mitigazione (overhead zero, bias di rumore completo). Il parametro bias_tolerance controlla questa manopola: impostarlo a 0 ripristina la PEC completa, mentre valori più grandi consentono più bias residuo in cambio di un overhead di campionamento γ² inferiore.

Questo passaggio fa variare bias_tolerance su un intervallo di valori e calcola l'overhead di campionamento risultante per ciascuno. Rappresentare graficamente questi punti rispetto all'overhead della PEC completa ti aiuta a scegliere un punto di lavoro adatto al tuo budget di shot. Ad esempio, tollerare un bias residuo del 5 % può ridurre l'overhead di campionamento di un ordine di grandezza rispetto alla PEC completa, rendendo il calcolo fattibile sulle QPU odierne dove altrimenti non lo sarebbe.

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

Scegli il limite di bias desiderato in base al compromesso tra overhead di shot e bias​

In questa guida scegliamo una tolleranza di bias del 5%. Nota che questo calcolo del bias si basa sul modello di rumore appreso, che potrebbe non essere perfettamente allineato con il rumore reale del dispositivo.

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
)

Prepara un programma PEC + SLC, eseguilo ed effettua il post-processing​

Con le scale locali scelte, possiamo costruire il programma di esecuzione completo PEC+SLC. La chiamata PEC.prepare costruisce un QuantumProgram che codifica la strategia di iniezione dell'anti-rumore nelle annotazioni dei box del circuito. Passare noise_gain="slc_local_scales" indica all'executor di usare i pesi per errore impostati da set_slc_local_scales, anziché applicare a ogni errore lo stesso guadagno di rumore inverso completo come farebbe la PEC standard.

Per produrre un confronto equo, viene preparato un secondo task PEC con noise_gain=0, che corrisponde alla cancellazione PEC completa senza tolleranza di bias SLC. Entrambi i programmi vengono aggiunti allo stesso QuantumProgram così da condividere un unico job di esecuzione: i loro circuiti vengono interlacciati ed eseguiti insieme, il che significa che qualsiasi deriva dell'hardware li influenza in egual misura.

Al termine del job, postprocess converte le stringhe di bit grezze delle misure in valori di aspettazione, applicando per ciascun task l'opportuno fattore di riscalamento 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)

Confronta i valori di aspettazione mitigati e l'overhead di shot​

Il valore di aspettazione ideale del circuito speculare è 1 per costruzione, quindi il bias di qualsiasi stima è semplicemente |1 - result|. Questa cella finale stampa il bias e il numero totale di shot per tre scenari affiancati:

  • Non mitigato: il valore di aspettazione rumoroso grezzo senza alcuna cancellazione degli errori.

  • PEC completa: cancellazione degli errori a bias zero che usa il modello di rumore inverso completo su ogni layer, con l'intero overhead di campionamento γ².

  • PEC+SLC: cancellazione degli errori guidata dalle scale locali shaded-lightcone, tollerando la bias_tolerance scelta in cambio di un numero di shot sostanzialmente inferiore.

Il confronto delle tre righe mostra il vantaggio concreto di SLC: il bias mitigato di PEC+SLC resta ben al di sotto del bias non mitigato, mentre il numero totale di shot è significativamente inferiore a quello richiesto dalla PEC completa.

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