PEC cu TREX
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).
Twirled Readout Error eXtinction (TREX) este o tehnică ce atenuează erorile de măsurare și este utilă pentru estimarea valorilor de așteptare ale observabilelor Pauli. Se bazează pe măsurarea cu twirling (injectarea aleatorie a unei porți X înainte de măsurare și inversarea rezultatului măsurat), care diagonalizează matricea de transfer a erorii de citire și o face ușor de inversat. Un termen de rescalare este învățat din canalul de zgomot diagonal prin măsurarea unor circuite de calibrare cu twirling inițializate în starea zero.
Acest ghid arată cum să folosești PEC pentru a atenua zgomotul porților și cum să îl combini cu TREX pentru a atenua și zgomotul de măsurare la estimarea valorilor de așteptare ale observabilelor.
Fluxul de lucru este următorul:
- Specifică un circuit și o observabilă.
- Calculează valorile de așteptare exacte folosind simulatorul de vector de stare al Qiskit.
- Calculează valorile de așteptare cu zgomot, nemitigate.
- Învață zgomotul straturilor de încurcare folosind qiskit-noise-learning.
- Pregătește un program PEC + TREX și adaugă-i un circuit de calibrare TREX.
- Compară valorile de așteptare mitigate.
Specifică un circuit și o observabilă
Pregătește un circuit oglindă al unei evoluții în timp Trotterizate a unui model Ising liniar cu câmp transversal pe opt qubiți. Oglindește circuitul pentru a anula unitarul circuitului și a forma un circuit identitate. Astfel poți calcula ușor valoarea de așteptare ideală a fiecărei observabile. În plus, oglindirea dublează adâncimea circuitului, ceea ce dublează la rândul său efectul zgomotului.
Folosește unghiul pentru a păstra toate porțile din circuit în grupul Clifford, permițând o simulare mai rapidă cu simulatorul 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
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ă un model de zgomot Pauli-Lindblad aleatoriu pe fiecare strat de porți de încurcare, generat cu random_pauli_lindblad_noise din qiskit-mitigation, cu o eroare per poartă în strat (EPLG) de 0.003, tipică pentru QPU-urile Heron actuale.
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()
Învață zgomotul straturilor de încurcare
PEC începe prin a învăța modelul 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.
Mitigarea zgomotului de citire se face în post-procesare folosind TREX, spre deosebire de modelul de zgomot al porților, care este necesar pentru pregătirea programului, pentru a injecta anti-zgomotul corespunzător modelului de zgomot învățat. Prin urmare, zgomotul de citire poate fi executat și învățat fie odată cu învățarea zgomotului porților, fie la executarea programului mitigat. Învățarea zgomotului de citire alături de cel al porților poate fi benefică pentru a învăța un model de zgomot auto-consistent (vezi, de exemplu, Disambiguating Pauli noise in quantum computers). În cazurile în care zgomotul porților și zgomotul de citire sunt separate în modelul de zgomot, se recomandă să înveți zgomotul de citire la executarea programului mitigat, pentru a evita deriva zgomotului între momentul în care a fost învățat și cel în care a fost executat.
În acest ghid, zgomotul de citire este învățat în timpul executării programului mitigat, folosind circuitul de calibrare 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()
Pregătește un program combinat PEC și TREX
Aici pregătim atât un task TREX, cât și unul PEC, și adăugăm task-ul TREX ca variabilă la task-ul PEC. Astfel ne asigurăm că toate proprietățile necesare mitigării TREX sunt adăugate elementului mitigat PEC (precum twirling-ul măsurătorilor). Zgomotul injectat este eșantionat o dată per randomizare, deci eroarea statistică a PEC este controlată de numărul de randomizări, nu de numărul de shot-uri per randomizare. Astfel, se folosesc multe randomizări - fiecare cu puține shot-uri - păstrând numărul total de shot-uri identic cu cel al programului nemitigat.
Circuitul de calibrare TREX este adăugat programului pentru a învăța zgomotul de citire în timpul executării programului. Circuitul de calibrare TREX este foarte puțin adânc, ceea ce face ca randomizările suplimentare pentru el să fie relativ ieftine și permite învățarea mai precisă a zgomotului de citire.
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)
Examinează overhead-ul de shot-uri, execută programul și efectuează post-procesarea
PEC returnează o estimare nepărtinitoare a valorii de așteptare, cu prețul unui overhead mare de shot-uri. Overhead-ul de shot-uri necesar poate fi aproximat calculând factorul gamma al circuitului, care reflectă efectul modelului de zgomot învățat asupra circuitului. Implicit, numărul de randomizări ale task-ului PEC este scalat cu cantitatea aproximativă de overhead de shot-uri necesară pentru o estimare nepărtinitoare (~). Pentru a evita această scalare automată, poți seta parametrul scale_randomizations_by_gamma la False. Overhead-ul de shot-uri este plafonat de parametrul max_sampling_overhead.
Ar trebui să verifici numărul de shot-uri necesar PEC pentru a atinge o estimare nepărtinitoare înainte de execuție, pentru a decide dacă această cantitate este accesibilă, deoarece crește exponențial cu ratele de zgomot și adâncimea circuitului.
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
)
Compară valorile de așteptare mitigate
În final, reprezintă grafic valoarea de așteptare a fiecărei observabile. PEC + TREX recuperează valorile de așteptare exacte în limitele barelor de eroare, în timp ce rezultatul nemitigat este părtinit mult sub ele.
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()