PEC z TREX
Probabilistyczne usuwanie błędów (PEC) to technika łagodzenia szumu kwantowego w obliczeniach wartości oczekiwanych obserwabli. Pozwala obliczyć nieobciążone oszacowanie wartości oczekiwanych. Technika ta polega na wykonaniu zespołu zaszumionych obwodów, w które wstrzyknięto odwrotność nauczonego modelu szumu. Model szumu jest następnie przybliżany kanałem Pauliego za pomocą twirlingu Pauliego (znanego też jako randomized compiling). Takie modele szumu można uczyć się efektywnie i można je łagodzić za pomocą PEC (więcej szczegółów w Probabilistic error cancellation with sparse Pauli-Lindblad models on noisy quantum processors).
Twirled Readout Error eXtinction (TREX) to technika łagodząca błędy pomiaru, przydatna do szacowania wartości oczekiwanych obserwabli Pauliego. Opiera się na twirlingu pomiarów (losowym wstrzykiwaniu bramki X przed pomiarem i odwracaniu zmierzonego wyniku), który diagonalizuje macierz przejścia błędów odczytu i ułatwia jej odwrócenie. Człon przeskalowujący jest uczony z diagonalnego kanału szumu przez pomiar obwodów kalibracyjnych po twirlingu, zainicjowanych w stanie zero.
Ten przewodnik pokazuje, jak użyć PEC do łagodzenia szumu bramek i połączyć je z TREX, aby łagodzić także szum pomiarów przy szacowaniu wartości oczekiwanych obserwabli.
Przebieg pracy jest następujący:
- Określ obwód i obserwablę.
- Oblicz dokładne wartości oczekiwane za pomocą symulatora wektora stanu Qiskit.
- Oblicz zaszumione wartości oczekiwane bez mitygacji.
- Naucz się szumu warstw splątujących za pomocą qiskit-noise-learning.
- Przygotuj program PEC + TREX i dodaj do niego obwód kalibracyjny TREX.
- Porównaj zmitygowane wartości oczekiwane.
Określenie obwodu i obserwabli
Przygotuj obwód lustrzany trotteryzowanej ewolucji w czasie liniowego modelu Isinga z polem poprzecznym na ośmiu kubitach. Obwód odbijamy lustrzanie, aby zniwelować jego operator unitarny i uzyskać obwód tożsamościowy. Dzięki temu łatwo obliczysz idealną wartość oczekiwaną każdej obserwabli. Dodatkowo odbicie lustrzane podwaja głębokość obwodu, co z kolei podwaja wpływ szumu.
Użyj kąta , aby wszystkie bramki w obwodzie pochodziły z grupy Clifforda, co pozwala na szybszą symulację za pomocą symulatora 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
Obliczenie zaszumionych wartości oczekiwanych bez mitygacji
QuantumProgram zawiera obwód kwantowy i samplex w formacie odpowiednim dla prymitywu Executor. MitigationTask.prepare buduje QuantumProgram z obwodu, obserwabli i wartości parametrów oraz otacza obwód boxami zgodnie z podanymi opcjami boxingu. Ustaw opcję boxingu measure_annotations na "change_basis", aby obrócić pomiary do wymaganych baz, i dodaj add_tags: "unique_box", aby symulator mógł odwzorować model szumu na boxy obwodu. Program domyślnie przechodzi twirling Pauliego, który na sprzęcie zamienia szum koherentny w szum Pauliego, jakiego zakładają metody mitygacji. num_randomizations określa, ile instancji obwodu po twirlingu jest próbkowanych dla każdego obwodu, a shots_per_randomization określa, ile strzałów otrzymuje każda instancja, więc ich iloczyn jest limitem strzałów na obwód.
Jeśli masz dostęp do QPU, możesz wykonać program za pomocą Executor zbudowanego z backendu QiskitRuntimeService. Ten przykład używa AerSimulator do lokalnego uruchomienia programu. Jego opcja symulatora layer_noise_model wstrzykuje losowy model szumu Pauliego-Lindblada do każdej warstwy bramek splątujących, wygenerowany za pomocą random_pauli_lindblad_noise z qiskit-mitigation z błędem na warstwę bramek (EPLG) równym 0.003, typowym dla obecnych 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()
Poznanie szumu warstw splątujących
PEC zaczyna od nauczenia się modelu szumu po twirlingu dla każdej warstwy bramek splątujących w obwodzie. Jeśli zakładasz markowowski model szumu, przyjmuje się, że głębokość obwodu nie wpływa na kanał szumu danej warstwy splątującej. Dlatego należy nauczyć się tylko modelu szumu każdej unikalnej warstwy bramek splątujących. Obwód użyty w tym przewodniku można podzielić na dwie unikalne warstwy. Projektowanie obwodów tak, aby miały minimalny zestaw unikalnych warstw splątujących, jest ważne, jeśli chcesz uczyć się modelu szumu, ponieważ unikalne warstwy trzeba uczyć niezależnie. Jeśli w obwodzie jest zbyt wiele unikalnych warstw, uczenie się szumu może stać się niewykonalne z powodu kosztu próbkowania przy uczeniu się wielu warstw.
Do uczenia się szumu używany jest qiskit-noise-learning. Learner powtarza każdą warstwę fragment_depths razy w swoich obwodach uczących, wykonuje twirling każdego obwodu num_randomizations razy i dopasowuje zanik zmierzonych wierności do modelu Pauliego-Lindblada. Parametry te mogą wpływać na dokładność nauczonego modelu. Im dokładniejszy nauczony model szumu, tym dokładniejsze będzie usuwanie szumu.
Poniższy kod rysuje nauczony model szumu z dwiema warstwami połączonymi w jedną mapę. Każdy kubit jest wykresem kołowym swoich współczynników X, Y i Z o wadze 1, zsumowanych po obu warstwach, a każda krawędź pokazuje nauczone współczynniki o wadze 2. Na przykład współczynnik 3e-4 oznacza, że błąd ten występuje średnio przy około trzech na każde 10 000 użyć warstwy.
Mitygacja szumu odczytu odbywa się w przetwarzaniu końcowym za pomocą TREX, w odróżnieniu od modelu szumu bramek, który jest wymagany do przygotowania programu, aby wstrzyknąć antyszum odpowiadający nauczonemu modelowi szumu. Dlatego szum odczytu można wykonać i poznać podczas uczenia się szumu bramek albo podczas wykonywania programu z mitygacją. Uczenie się szumu odczytu razem z szumem bramek może być korzystne dla uzyskania spójnego modelu szumu (zobacz na przykład Disambiguating Pauli noise in quantum computers). W przypadkach, gdy szum bramek i szum odczytu są w modelu szumu rozdzielone, zaleca się uczyć się szumu odczytu podczas wykonywania programu z mitygacją, aby uniknąć dryfu szumu między momentem jego poznania a wykonaniem.
W tym przewodniku szum odczytu jest poznawany podczas wykonywania programu z mitygacją za pomocą obwodu kalibracyjnego 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()
Przygotowanie połączonego programu PEC i TREX
Tutaj przygotowujemy zarówno zadanie TREX, jak i PEC, i dodajemy zadanie TREX jako zmienną do zadania PEC. Dzięki temu wszystkie właściwości wymagane do mitygacji TREX (takie jak twirling pomiarów) są dodawane do elementu z mitygacją PEC. Wstrzykiwany szum jest próbkowany raz na randomizację, więc błąd statystyczny PEC zależy od liczby randomizacji, a nie od liczby strzałów na randomizację. Dlatego używa się wielu randomizacji — każda z kilkoma strzałami — utrzymując łączną liczbę strzałów taką samą jak w programie bez mitygacji.
Obwód kalibracyjny TREX jest dodawany do programu, aby uczyć się szumu odczytu podczas jego wykonywania. Obwód kalibracyjny TREX jest bardzo płytki, dzięki czemu dodatkowe randomizacje są dla niego stosunkowo tanie i pozwalają dokładniej poznać szum odczytu.
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)
Sprawdzenie narzutu strzałów, wykonanie programu i przetwarzanie końcowe
PEC zwraca nieobciążone oszacowanie wartości oczekiwanej kosztem dużego narzutu strzałów. Wymagany narzut strzałów można przybliżyć, obliczając współczynnik gamma obwodu, który odzwierciedla wpływ nauczonego modelu szumu na obwód. Domyślnie liczba randomizacji zadania PEC jest skalowana przybliżonym narzutem strzałów wymaganym do nieobciążonego oszacowania (~). Aby uniknąć tego automatycznego skalowania, możesz ustawić parametr scale_randomizations_by_gamma na False. Narzut strzałów jest ograniczony parametrem max_sampling_overhead.
Przed wykonaniem powinieneś sprawdzić liczbę strzałów wymaganych przez PEC do osiągnięcia nieobciążonego oszacowania, aby zdecydować, czy taka ilość jest do udźwignięcia, ponieważ rośnie ona wykładniczo ze współczynnikami szumu i głębokością obwodu.
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
)
Porównanie zmitygowanych wartości oczekiwanych
Na koniec narysuj wartość oczekiwaną każdej obserwabli. PEC + TREX odtwarza dokładne wartości oczekiwane w granicach swoich słupków błędów, podczas gdy wynik bez mitygacji jest obciążony i leży znacznie poniżej nich.
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()