PEC con TREX
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).
Twirled Readout Error eXtinction (TREX) è una tecnica che mitiga gli errori di misura ed è utile per stimare i valori di aspettazione di osservabili di Pauli. Si basa sul twirling della misura (iniettando casualmente un gate X prima della misura e invertendo il risultato misurato), che diagonalizza la matrice di trasferimento dell'errore di lettura e ne rende facile l'inversione. Un termine di riscalamento viene appreso dal canale di rumore diagonale misurando circuiti di calibrazione twirled inizializzati nello stato zero.
Questa guida mostra come usare la PEC per mitigare il rumore dei gate e combinarla con TREX per mitigare anche il rumore di misura nella stima dei valori di aspettazione delle osservabili.
Il flusso di lavoro è il seguente:
- Specifica un circuito e un'osservabile.
- Calcola i valori di aspettazione esatti usando il simulatore di vettore di stato di Qiskit.
- Calcola i valori di aspettazione rumorosi e non mitigati.
- Apprendi il rumore dei layer di entanglement usando qiskit-noise-learning.
- Prepara un programma PEC + TREX e aggiungi ad esso un circuito di calibrazione TREX.
- Confronta i valori di aspettazione mitigati.
Specifica un circuito e un'osservabile
Prepara un circuito speculare di un'evoluzione temporale trotterizzata di un modello di Ising lineare a campo trasverso su otto qubit. Rispecchia il circuito per annullarne l'unitaria e formare un circuito identità. Questo ti permette di calcolare facilmente il valore di aspettazione ideale di ciascuna osservabile. Inoltre, lo specchio raddoppia la profondità del circuito, il che a sua volta raddoppia l'effetto del rumore.
Usa l'angolo per mantenere tutti i gate del circuito nel gruppo di Clifford, consentendo una simulazione più veloce con il simulatore stabilizer.
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "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
warnings.filterwarnings("ignore")
def construct_ising_circuit(
num_qubits: int,
num_trotter_steps: int,
rx_angle: float,
barrier: bool = True,
) -> QuantumCircuit:
circuit = QuantumCircuit(num_qubits)
for _step in range(num_trotter_steps):
circuit.rx(rx_angle, range(num_qubits))
if barrier:
circuit.barrier()
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)
if barrier:
circuit.barrier()
return circuit
def mirror_circuit(circuit: QuantumCircuit) -> QuantumCircuit:
mirror_circ = circuit.copy_empty_like()
mirror_circ.compose(circuit, inplace=True)
mirror_circ.barrier()
mirror_circ.compose(circuit.inverse(), inplace=True)
mirror_circ.measure_active()
return mirror_circ
# define observables
num_qubits = 8
observables = [
SparsePauliOp("I" * ((num_qubits - 1) - q) + "Z" + "I" * q)
for q in range(num_qubits)
]
# Instantiate circuit
num_trotter_steps = 5
rx_angle = np.pi / 2
circuit = construct_ising_circuit(
num_qubits, num_trotter_steps, rx_angle, barrier=False
)
mirrored_circuit = mirror_circuit(circuit)
print("Visualization of 1 mirrored Trotter step")
mirror_circuit(
construct_ising_circuit(num_qubits, 1, rx_angle, barrier=False)
).draw("mpl", scale=0.7)
Visualization of 1 mirrored Trotter step
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 un modello di rumore di Pauli-Lindblad casuale su ogni layer di gate di entanglement, generato con random_pauli_lindblad_noise di qiskit-mitigation con un errore per gate a livello di layer (EPLG) di 0.003, tipico delle attuali QPU Heron.
import matplotlib.pyplot as plt
from qiskit.circuit import CircuitInstruction
from qiskit.quantum_info import PauliLindbladMap, pauli_basis
from qiskit.transpiler import PassManager
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import Executor
from qiskit_mitigation import MitigationTask, find_combined_unique_layers
from qiskit_noise_learning.aer_executor.insert_noise_pass import (
InsertNoisePass,
)
from samplomatic import Tag
from samplomatic.utils import get_annotation
def generate_random_pauli_lindblad_noise(
layers,
num_qubits: int,
eplg: float = 3e-3,
readout_noise_scale: float = 3e-2,
seed: int | None = None,
) -> tuple[
list[tuple[CircuitInstruction, PauliLindbladMap]], 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 not contain_measurement:
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))
# generate a readout noise model
rng = np.random.default_rng(seed=seed)
rates = rng.random(num_qubits) * readout_noise_scale
readout_noise_model = PauliLindbladMap.from_sparse_list(
[("X", [q], rate) for q, rate in enumerate(rates)],
num_qubits=num_qubits,
)
return layer_noise_model, readout_noise_model
def add_readout_noise(program, readout_noise_model):
"""Injects readout noise to the circuits in a quantum program."""
readout_noise_map = {}
for item in program.items:
readout_tag = item.circuit[-1].label[
-4:
] # extract the readout layer tag from the circuit
readout_noise_map[readout_tag] = readout_noise_model
item.circuit = PassManager(
[InsertNoisePass(noise_dict=readout_noise_map, noise_after=False)]
).run(item.circuit)
num_randomizations = 300
shots_per_randomization = 64
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, readout_noise_model = generate_random_pauli_lindblad_noise(
box_layers, num_qubits, seed=42
)
# a local noisy simulator in place of a QPU backend
noisy_executor = Executor(
AerSimulator(method="stabilizer"),
options={"simulator": {"layer_noise_model": layer_noise_model}},
)
task = MitigationTask()
program = task.prepare(
circuit=mirrored_circuit,
observables=observables,
parameters=None,
num_randomizations=num_randomizations,
shots_per_randomization=shots_per_randomization,
custom_boxing_options=boxing_options,
)
# For the simulation - we inject readout noise to the circuits of the program
add_readout_noise(program, readout_noise_model)
program._semantic_role = "simulation" # flag the program for the simulator
unmitigated_res = task.postprocess(noisy_executor.run(program).result())
x_vals = np.array(range(num_qubits))
fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(x_vals, [1] * len(x_vals), "k-", alpha=0.5, label="Exact")
ax.errorbar(
x_vals,
unmitigated_res.data.evs,
unmitigated_res.data.stds,
fmt="o",
label="Unmitigated",
)
x_tick_labels = [r"$Z_{ {i} }$" for i in range(num_qubits)]
ax.set_xlabel(r"$\theta/\pi$")
ax.set_ylabel("Expectation value")
ax.legend()
ax.set_xticks(x_vals, x_tick_labels)
plt.tight_layout()
plt.show()
Apprendi il rumore dei layer di entanglement
La PEC inizia apprendendo il 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.
La mitigazione del rumore di lettura viene eseguita nel post-processing usando TREX, a differenza del modello di rumore dei gate, che è necessario per la preparazione del programma al fine di iniettare l'anti-rumore corrispondente al modello di rumore appreso. Pertanto, il rumore di lettura può essere eseguito e appreso durante l'apprendimento del rumore dei gate, oppure durante l'esecuzione del programma mitigato. Apprendere il rumore di lettura insieme al rumore dei gate può essere utile per apprendere un modello di rumore autoconsistente (vedi ad esempio Disambiguating Pauli noise in quantum computers). Nei casi in cui il rumore dei gate e il rumore di lettura sono separati nel modello di rumore, si consiglia di apprendere il rumore di lettura durante l'esecuzione del programma mitigato, per evitare la deriva del rumore tra il momento in cui è stato appreso e quello in cui viene eseguito.
In questa guida, il rumore di lettura viene appreso durante l'esecuzione del programma mitigato usando il circuito di calibrazione TREX.
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_mitigation import PEC
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,
)
task_pec = PEC()
# first learn only the layers PEC will inject noise into, i.e. not the measurement layer
pec_layers = [
layer
for layer in task_pec.find_unique_layers(mirrored_circuit, boxing_options)
if get_annotation(layer.operation, InjectNoise)
]
layers = task_pec.find_unique_layers(mirrored_circuit, 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(method="stabilizer"), 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()
Prepara un programma PEC e TREX combinato
Qui prepariamo sia un task TREX sia un task PEC, e aggiungiamo il task TREX come variabile al task PEC. Questo garantisce che tutte le proprietà necessarie per la mitigazione TREX vengano aggiunte all'elemento mitigato con PEC (come il twirling della misura). Il rumore iniettato viene campionato una volta per randomizzazione, quindi l'errore statistico della PEC è controllato dal numero di randomizzazioni anziché dagli shot per randomizzazione. Pertanto vengono usate molte randomizzazioni, ciascuna con pochi shot, mantenendo il numero totale di shot uguale a quello del programma non mitigato.
Il circuito di calibrazione TREX viene aggiunto al programma per apprendere il rumore di lettura durante l'esecuzione del programma. Il circuito di calibrazione TREX è molto poco profondo, il che rende relativamente economiche le randomizzazioni aggiuntive per esso e consente di apprendere il rumore di lettura con maggiore accuratezza.
from qiskit_mitigation import TREX
trex = TREX()
program_pec = task_pec.prepare(
circuit=mirrored_circuit,
observables=observables,
parameters=None,
num_randomizations=1200,
shots_per_randomization=16,
custom_boxing_options=boxing_options,
trex=trex,
noise_maps=learned_noise, # PEC option
noise_gain=0, # PEC option
max_sampling_overhead=100, # PEC option
)
program_pec = trex.prepare(
num_randomizations=1500,
quantum_program=program_pec,
custom_boxing_options=boxing_options,
)
# For the simulation - we inject readout noise to the circuits of the program
add_readout_noise(program_pec, readout_noise_model)
Esamina l'overhead di shot, esegui il programma ed effettua il post-processing
La PEC restituisce una stima non distorta del valore di aspettazione, al costo di un grande overhead di shot. L'overhead di shot richiesto può essere approssimato calcolando il fattore gamma del circuito, che riflette l'effetto del modello di rumore appreso sul circuito. Per impostazione predefinita, il numero di randomizzazioni del task PEC viene scalato in base alla quantità approssimativa di overhead di shot necessaria per avere una stima non distorta (~). Per evitare questa scalatura automatica, puoi impostare il parametro scale_randomizations_by_gamma su False. L'overhead di shot è limitato dal parametro max_sampling_overhead.
Dovresti verificare il numero di shot richiesti dalla PEC per raggiungere una stima non distorta prima dell'esecuzione, per decidere se questa quantità è sostenibile, poiché scala esponenzialmente con i tassi di rumore e con la profondità del circuito.
print(f"The circuit calculated gamma is: {task_pec.gamma}")
print(f"The circuit shot overhead is ~{task_pec.gamma**2}")
The circuit calculated gamma is: 1.2774846896089953
The circuit shot overhead is ~1.631967132185391
program_pec._semantic_role = (
"simulation" # flag the program for the simulator
)
job_pec_results = noisy_executor.run(program_pec).result()
# post-processing
pec_res = task_pec.postprocess(job_pec_results)
# In order to post process the results without TREX, we call `postprocess` again, but instead of the default options we
# pass only the PEC item result and the `measure_noise_data` as `None`
pec_without_trex_res = task_pec.postprocess(
job_pec_results[task_pec._program_item_index], measure_noise_data=None
)
Confronta i valori di aspettazione mitigati
Infine, rappresenta graficamente il valore di aspettazione di ciascuna osservabile. PEC + TREX recupera i valori di aspettazione esatti entro le sue barre di errore, mentre il risultato non mitigato è affetto da un bias che lo porta molto al di sotto.
x = np.array(range(num_qubits))
x_tick_labels = [r"$Z_{ {i} }$" for i in range(num_qubits)]
fig, ax = plt.subplots(figsize=(8, 5))
ax.errorbar(
x,
unmitigated_res.data.evs,
unmitigated_res.data.stds,
fmt="o",
label="Twirl only",
)
ax.errorbar(
x,
pec_without_trex_res.data.evs,
pec_without_trex_res.data.stds,
fmt="o",
label="PEC only",
)
ax.errorbar(
x,
pec_res.data.evs,
pec_res.data.stds,
fmt="o",
label="PEC + TREX",
)
ax.plot(x, [1] * len(x), "--", color="black", alpha=0.5, label="Exact")
tick_labels = [f"$Z_{ {i} }$" for i in range(num_qubits)]
ax.set_xticks(x, tick_labels)
ax.set_ylabel("Expectation value")
ax.legend()
ax.set_title("PEC+TREX Mitigated Expectation Values")
plt.tight_layout()
plt.show()