Łączenie opcji łagodzenia błędów z prymitywem Estimator
Szacowane użycie: siedem minut na procesorze Heron r2 (UWAGA: To tylko szacunek. Twój czas wykonania może się różnić.)
Tło
Ten przewodnik omawia opcje tłumienia błędów i łagodzenia błędów dostępne w prymitywie Estimator z Qiskit Runtime. Zbudujesz Circuit i obserwowalną, a następnie wyślesz zadania za pomocą prymitywu Estimator, korzystając z różnych kombinacji ustawień łagodzenia błędów. Następnie wykreślisz wyniki, aby zaobserwować efekty poszczególnych ustawień. Większość przykładów używa obwodu 10-qubitowego, aby ułatwić wizualizacje, a na końcu możesz skalować przepływ pracy do 50 Qubitów.
Oto opcje tłumienia i łagodzenia błędów, których użyjesz:
- Dynamiczne sprzęganie (Dynamical decoupling)
- Łagodzenie błędów pomiarowych
- Twirling bramek (Gate twirling)
- Ekstrapolacja zerowego szumu (ZNE)
Wymagania
Przed rozpoczęciem tego przewodnika upewnij się, że masz zainstalowane:
- Qiskit SDK v2.1 lub nowszy, z obsługą wizualizacji
- Qiskit Runtime v0.40 lub nowszy (
pip install qiskit-ibm-runtime)
Konfiguracja
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-runtime
import matplotlib.pyplot as plt
import numpy as np
from qiskit.circuit.library import efficient_su2, unitary_overlap
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Batch, EstimatorV2 as Estimator
Krok 1: Mapowanie klasycznych danych wejściowych na problem kwantowy
Ten przewodnik zakłada, że klasyczny problem został już zmapowany na problem kwantowy. Zacznij od zbudowania Circuit i obserwowalnej do zmierzenia. Chociaż techniki stosowane tutaj mają zastosowanie do wielu różnych rodzajów obwodów, dla uproszczenia ten przewodnik używa obwodu efficient_su2 zawartego w bibliotece obwodów Qiskit.
efficient_su2 to sparametryzowany obwód kwantowy zaprojektowany tak, aby być efektywnie wykonywalnym na sprzęcie kwantowym z ograniczoną łącznością Qubitów, a jednocześnie wystarczająco ekspresywnym do rozwiązywania problemów w takich dziedzinach zastosowań jak optymalizacja i chemia. Zbudowany jest przez naprzemienne warstwy sparametryzowanych bramek jednokubitowych z warstwą zawierającą stały wzorzec bramek dwukubitowych, dla wybranej liczby powtórzeń. Wzorzec bramek dwukubitowych może być określony przez użytkownika. Możesz tu użyć wbudowanego wzorca pairwise, ponieważ minimalizuje on głębokość Circuit, pakując bramki dwukubitowe możliwie gęsto. Ten wzorzec może być wykonany przy użyciu tylko liniowej łączności Qubitów.
n_qubits = 10
reps = 1
circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)
circuit.decompose().draw("mpl", scale=0.7)


Jako obserwowalną przyjmijmy operator Pauliego działający na ostatnim Qubicie, .
# Z on the last qubit (index -1) with coefficient 1.0
observable = SparsePauliOp.from_sparse_list(
[("Z", [-1], 1.0)], num_qubits=n_qubits
)
W tym momencie możesz przystąpić do uruchomienia Circuit i zmierzenia obserwowalnej. Chcesz jednak również porównać wyniki urządzenia kwantowego z poprawną odpowiedzią — czyli teoretyczną wartością obserwowalnej, gdyby Circuit był wykonany bez błędów. Dla małych obwodów kwantowych możesz obliczyć tę wartość, symulując Circuit na komputerze klasycznym, ale nie jest to możliwe dla większych obwodów w skali użytkowej. Możesz obejść ten problem za pomocą techniki „lustrzanego obwodu" (ang. „mirror circuit", zwanej też „compute-uncompute"), która jest przydatna do oceny wydajności urządzeń kwantowych.
Lustrzany obwód
W technice lustrzanego obwodu łączysz Circuit z jego odwrotnym obwodem, który tworzy się przez odwrócenie każdej bramki w odwrotnej kolejności. Powstały Circuit implementuje operator identyczności, który można trywialnie zasymulować. Ponieważ struktura oryginalnego Circuit jest zachowana w lustrzanym obwodzie, wykonanie lustrzanego Circuit daje nadal wyobrażenie o tym, jak urządzenie kwantowe zachowałoby się na oryginalnym obwodzie.
Poniższa komórka kodu przypisuje losowe parametry do twojego Circuit, a następnie konstruuje lustrzany obwód za pomocą klasy unitary_overlap. Przed wykonaniem lustrzania obwodu dodaj do niego instrukcję bariery, aby zapobiec łączeniu przez Transpiler obu części Circuit po obu stronach bariery. Bez bariery Transpiler połączyłby oryginalny Circuit z jego odwrotnością, co skutkowałoby transpilowanym obwodem bez żadnych bramek.
# Generate random parameters
rng = np.random.default_rng(1234)
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)
# Assign the parameters to the circuit
assigned_circuit = circuit.assign_parameters(params)
# Add a barrier to prevent circuit optimization of mirrored operators
assigned_circuit.barrier()
# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)
mirror_circuit.decompose().draw("mpl", scale=0.7)


Krok 2: Optymalizacja problemu do wykonania na sprzęcie kwantowym
Musisz zoptymalizować swój Circuit przed uruchomieniem go na sprzęcie. Proces ten obejmuje kilka kroków:
- Wybór układu Qubitów, który mapuje wirtualne Qubity twojego Circuit na fizyczne Qubity sprzętu.
- Wstawianie bramek swap w razie potrzeby, aby kierować interakcje między Qubitami, które nie są połączone.
- Tłumaczenie bramek w twoim Circuit na instrukcje Instruction Set Architecture (ISA), które mogą być bezpośrednio wykonywane na sprzęcie.
- Wykonanie optymalizacji Circuit w celu zminimalizowania głębokości i liczby bramek.
Transpiler wbudowany w Qiskit może wykonać wszystkie te kroki za ciebie. Ponieważ ten przykład używa obwodu efektywnego sprzętowo, Transpiler powinien być w stanie wybrać układ Qubitów, który nie wymaga wstawiania żadnych bramek swap do kierowania interakcji.
Przed optymalizacją Circuit musisz wybrać urządzenie sprzętowe. Poniższa komórka kodu żąda najmniej zajętego urządzenia z co najmniej 127 Qubitami.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
Możesz transpilować swój Circuit dla wybranego Backend, tworząc menedżer przejść, a następnie uruchamiając go na obwodzie. Łatwym sposobem na stworzenie menedżera przejść jest użycie funkcji generate_preset_pass_manager. Więcej szczegółów na temat transpilacji z menedżerami przejść znajdziesz w Transpilacja z menedżerami przejść.
pass_manager = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=1234
)
isa_circuit = pass_manager.run(mirror_circuit)
isa_circuit.draw("mpl", idle_wires=False, scale=0.7, fold=-1)


Transpilowany Circuit zawiera teraz tylko instrukcje ISA. Bramki jednokubitowe zostały rozłożone w postaci bramek i rotacji , a bramki CX zostały rozłożone na bramki ECR i jednokubitowe rotacje.
Proces transpilacji zmapował wirtualne Qubity Circuit na fizyczne Qubity sprzętu. Informacje o układzie Qubitów są przechowywane w atrybucie layout transpilowanego Circuit. Obserwowalna była również zdefiniowana w kategoriach wirtualnych Qubitów, więc musisz zastosować ten układ do obserwowalnej, co możesz zrobić metodą apply_layout klasy SparsePauliOp.
isa_observable = observable.apply_layout(isa_circuit.layout)
print("Original observable:")
print(observable)
print()
print("Observable with layout applied:")
print(isa_observable)
Original observable:
SparsePauliOp(['ZIIIIIIIII'],
coeffs=[1.+0.j])
Observable with layout applied:
SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])
Krok 3: Wykonanie przy użyciu prymitywów Qiskit
Jesteś teraz gotowy, aby uruchomić swój Circuit za pomocą prymitywu Estimator.
Tutaj wyślesz pięć oddzielnych zadań, zaczynając od braku tłumienia i łagodzenia błędów, a następnie sukcesywnie włączając różne opcje tłumienia i łagodzenia błędów dostępne w Qiskit Runtime. Informacje o opcjach znajdziesz na następujących stronach:
- Przegląd wszystkich opcji
- Dynamiczne sprzęganie
- Odporność, w tym łagodzenie błędów pomiarowych i ekstrapolacja zerowego szumu (ZNE)
- Twirling
Ponieważ te zadania mogą być wykonywane niezależnie od siebie, możesz użyć trybu wsadowego, aby umożliwić Qiskit Runtime optymalizację harmonogramu ich wykonania.
pub = (isa_circuit, isa_observable)
jobs = []
with Batch(backend=backend) as batch:
estimator = Estimator(mode=batch)
# Set number of shots
estimator.options.default_shots = 100_000
# Disable runtime compilation and error mitigation
estimator.options.resilience_level = 0
# Run job with no error mitigation
job0 = estimator.run([pub])
jobs.append(job0)
# Add dynamical decoupling (DD)
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XpXm"
job1 = estimator.run([pub])
jobs.append(job1)
# Add readout error mitigation (DD + TREX)
estimator.options.resilience.measure_mitigation = True
job2 = estimator.run([pub])
jobs.append(job2)
# Add gate twirling (DD + TREX + Gate Twirling)
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"
job3 = estimator.run([pub])
jobs.append(job3)
# Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 3, 5)
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
job4 = estimator.run([pub])
jobs.append(job4)
Krok 4: Przetwarzanie końcowe i zwracanie wyniku w żądanym formacie klasycznym
Na koniec możesz przeanalizować dane. Pobierzesz wyniki zadań, wyodrębnisz z nich zmierzone wartości oczekiwane i narysujesz wykres z tymi wartościami, uwzględniając słupki błędów o długości jednego odchylenia standardowego.
# Retrieve the job results
results = [job.result() for job in jobs]
# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]
# Unpack the expectation values and standard errors
expectation_vals = np.array(
[float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
[float(pub_result.data.stds) for pub_result in pub_results]
)
# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
range(len(labels)),
expectation_vals,
yerr=standard_errors,
label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")
plt.show()
W tej małej skali trudno dostrzec efekt większości technik mitygacji błędów, jednak ekstrapolacja do zerowego szumu (ZNE) daje zauważalną poprawę. Warto jednak pamiętać, że ta poprawa nie jest bezkosztowa — wynik ZNE ma też większy słupek błędu.
Skalowanie eksperymentu
Podczas tworzenia eksperymentu warto zaczynać od małego obwodu, co ułatwia wizualizacje i symulacje. Po opracowaniu i przetestowaniu przepływu pracy na obwodzie 10-qubitowym możesz go teraz przeskalować do 50 qubitów. Poniższy kod powtarza wszystkie kroki z tego przewodnika, ale stosuje je do obwodu 50-qubitowego.
n_qubits = 50
reps = 1
# Construct circuit and observable
circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)
observable = SparsePauliOp.from_sparse_list(
[("Z", [-1], 1.0)], num_qubits=n_qubits
)
# Assign parameters to circuit
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)
assigned_circuit = circuit.assign_parameters(params)
assigned_circuit.barrier()
# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)
# Transpile circuit and observable
isa_circuit = pass_manager.run(mirror_circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)
# Run jobs
pub = (isa_circuit, isa_observable)
jobs = []
with Batch(backend=backend) as batch:
estimator = Estimator(mode=batch)
# Set number of shots
estimator.options.default_shots = 100_000
# Disable runtime compilation and error mitigation
estimator.options.resilience_level = 0
# Run job with no error mitigation
job0 = estimator.run([pub])
jobs.append(job0)
# Add dynamical decoupling (DD)
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XpXm"
job1 = estimator.run([pub])
jobs.append(job1)
# Add readout error mitigation (DD + TREX)
estimator.options.resilience.measure_mitigation = True
job2 = estimator.run([pub])
jobs.append(job2)
# Add gate twirling (DD + TREX + Gate Twirling)
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"
job3 = estimator.run([pub])
jobs.append(job3)
# Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 3, 5)
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
job4 = estimator.run([pub])
jobs.append(job4)
# Retrieve the job results
results = [job.result() for job in jobs]
# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]
# Unpack the expectation values and standard errors
expectation_vals = np.array(
[float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
[float(pub_result.data.stds) for pub_result in pub_results]
)
# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
range(len(labels)),
expectation_vals,
yerr=standard_errors,
label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")
plt.show()
Porównując wyniki dla 50 qubitów z wcześniejszymi wynikami dla 10 qubitów, możesz zauważyć (twoje wyniki mogą różnić się między uruchomieniami):
- Wyniki bez mitygacji błędów są gorsze. Uruchomienie większego obwodu wymaga wykonania większej liczby bramek, co daje więcej okazji do akumulowania błędów.
- Dodanie dynamicznego odsprzęgania (DD) mogło pogorszyć wydajność. Nie jest to zaskakujące, ponieważ obwód jest bardzo gęsty. Dynamiczne odsprzęganie jest przede wszystkim użyteczne, gdy w obwodzie istnieją duże przerwy, podczas których qubity bezczynnie czekają bez stosowania do nich bramek. Gdy takich przerw nie ma, dynamiczne odsprzęganie jest nieskuteczne, a z powodu błędów w samych pulsach DD może wręcz pogarszać wydajność. Obwód 10-qubitowy mógł być zbyt mały, aby zaobserwować ten efekt.
- Przy zastosowaniu ekstrapolacji do zerowego szumu (ZNE) wynik jest tak samo dobry, lub prawie tak samo dobry, jak wynik dla 10 qubitów — choć słupek błędu jest znacznie większy. To pokazuje siłę techniki ZNE!
Podsumowanie
W tym przewodniku zbadałeś różne opcje mitygacji błędów dostępne dla prymitywu Estimator z Qiskit Runtime. Opracowałeś przepływ pracy na obwodzie 10-qubitowym, a następnie przeskalowałeś go do 50 qubitów. Mogłeś zaobserwować, że włączanie kolejnych opcji tłumienia i mitygacji błędów nie zawsze poprawia wydajność (w tym przypadku konkretnie włączenie dynamicznego odsprzęgania). Większość opcji przyjmuje dodatkową konfigurację, którą możesz samodzielnie przetestować we własnej pracy!