Przejdź do głównej treści

Łączenie opcji łagodzenia błędów z prymitywem Estimator

Szacowany czas użycia: 7 minut na procesorze Heron r2 (UWAGA: to jedynie szacunek. Twój czas wykonania może się różnić.)

Efekty kształcenia

Sugerujemy, aby użytkownicy zapoznali się z poniższymi tematami przed przejściem przez ten samouczek:

  • Podstawy dynamicznego odsprzęgania, łagodzenia błędów pomiarowych, twirling bramek oraz ekstrapolacji zerowego szumu, opisane w tym przewodniku.

Wymagania wstępne

Po przejściu przez ten samouczek użytkownicy powinni rozumieć:

  • Jak wymienione techniki łagodzenia błędów są selektywnie implementowane na sprzęcie.
  • Jak porównują się pod względem zdolności do łagodzenia szumu sprzętowego.

Tło

Ten samouczek bada opcje tłumienia błędów i łagodzenia błędów dostępne w prymitywie Estimator z Qiskit Runtime. Samouczek pokazuje, jak implementować każdą z poniższych metod indywidualnie:

  • Dynamiczne odsprzęganie
  • Łagodzenie błędów pomiarowych
  • Twirling bramek
  • Ekstrapolacja zerowego szumu (ZNE)

Pamiętaj, że alternatywą dla implementowania tych technik indywidualnie jest ich implementowanie przy użyciu poziomu odporności, gdzie resilience_level przyjmuje wartości 0, 1, 2:

  • 0 : Brak łagodzenia.
  • 1 : Łagodzenie błędów pomiarowych jest implementowane.
  • 2 : Twirling bramek, łagodzenie błędów pomiarowych i ZNE są implementowane.

W tym samouczku zbudujesz Circuit i obserwowalną, a następnie prześlesz zadania używając prymitywu Estimator z różnymi kombinacjami ustawień łagodzenia błędów. Następnie wyrysujemy wyniki, aby zaobserwować efekty różnych ustawień. Większość samouczka używa 10-qubitowego Circuit, aby ułatwić wizualizacje, a na końcu skalujesz przepływ pracy do 50 qubitów.

Wymagania

Przed rozpoczęciem tego przewodnika upewnij się, że masz zainstalowane następujące elementy:

  • 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

Przykład na małą skalę z symulatorem

Pominiemy ten krok, ponieważ łagodzenie błędów w czasie wykonywania nie jest obsługiwane na symulatorach.

Przykład sprzętowy

Krok 1: Mapowanie klasycznych danych wejściowych na problem kwantowy

Ten przewodnik zakłada, że klasyczny problem został już zmapowany na kwantowy. Zacznij od skonstruowania Circuit i obserwowalnej do pomiaru. Chociaż stosowane tu techniki mają zastosowanie do wielu różnych rodzajów Circuit, dla uproszczenia ten przewodnik używa Circuit efficient_su2 zawartego w bibliotece Circuit Qiskit.

efficient_su2 to sparametryzowany Circuit kwantowy zaprojektowany tak, aby był efektywnie wykonywalny na sprzęcie kwantowym z ograniczoną łącznością qubitów, a jednocześnie wystarczająco ekspresyjny do rozwiązywania problemów w dziedzinach zastosowań takich jak optymalizacja i chemia. Jest zbudowany przez naprzemienne warstwy sparametryzowanych bramek jednokubitowych z warstwą zawierającą ustalony wzorzec bramek dwukubitowych, dla wybranej liczby powtórzeń. Wzorzec bramek dwukubitowych może być określony przez użytkownika. Tutaj możesz użyć wbudowanego wzorca pairwise, ponieważ minimalizuje głębokość Circuit przez jak najgęstsze upakowanie bramek dwukubitowych. 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)

Output of the previous code cell

Jako naszą obserwowalną weźmy operator Pauliego ZZ działający na ostatnim qubicie, ZIIZ I \cdots I. Pamiętaj, że fakt, że ostatni qubit odpowiada pierwszemu elementowi tego ciągu, wynika z użycia przez Qiskit notacji little-endian.

# 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 przejść do uruchomienia swojego Circuit i pomiaru obserwowalnej. Jednak chcesz też porównać wynik urządzenia kwantowego z poprawną odpowiedzią — czyli teoretyczną wartością obserwowalnej, gdyby Circuit był wykonany bez błędów. W przypadku małych Circuit kwantowych możesz obliczyć tę wartość, symulując Circuit na komputerze klasycznym, ale nie jest to możliwe dla większych Circuit w skali użytkowej. Możesz obejść ten problem za pomocą techniki "lustrzanego Circuit" (zwanej też "compute-uncompute"), która jest przydatna do oceny wydajności urządzeń kwantowych.

Lustrzany Circuit

W technice lustrzanego Circuit, łączysz Circuit z jego odwrotnym Circuit, który jest tworzony przez odwrócenie każdej bramki Circuit w odwrotnej kolejności. Wynikowy Circuit implementuje operator tożsamości, który można trywialnie symulować. Ponieważ struktura oryginalnego Circuit jest zachowana w lustrzanym Circuit, wykonanie lustrzanego Circuit daje nadal wyobrażenie o tym, jak urządzenie kwantowe zachowałoby się na oryginalnym Circuit.

Poniższy blok kodu przypisuje losowe parametry do twojego Circuit, a następnie konstruuje lustrzany Circuit używając klasy unitary_overlap. Przed lustrzanym odbiciem Circuit, dołącz do niego instrukcję barrier, aby zapobiec scalaniu przez Transpiler obu części Circuit po obu stronach bariery, co skutkowałoby transpilowanym Circuit 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)

Output of the previous code cell

Krok 2: Optymalizacja problemu pod kątem wykonania na sprzęcie kwantowym

Musisz zoptymalizować swój Circuit przed uruchomieniem go na sprzęcie. Proces ten obejmuje kilka kroków:

  • Wybierz układ qubitów, który mapuje wirtualne qubity twojego Circuit na fizyczne qubity sprzętu.
  • Wstawiaj bramki swap w razie potrzeby, aby kierować interakcje między qubitami, które nie są połączone.
  • Przetłumacz bramki w swoim Circuit na instrukcje Instruction Set Architecture (ISA), które mogą być bezpośrednio wykonane na sprzęcie.
  • Wykonaj optymalizacje Circuit, aby zminimalizować głębokość Circuit i liczbę bramek.

Transpiler wbudowany w Qiskit może wykonać wszystkie te kroki za ciebie. Ponieważ ten przykład używa Circuit wydajnego sprzętowo, Transpiler powinien być w stanie wybrać układ qubitów, który nie wymaga wstawiania żadnych bramek swap do kierowania interakcji.

Musisz wybrać urządzenie sprzętowe do użycia przed optymalizacją swojego Circuit. Poniższy blok 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
)
print(backend)
<IBMBackend('ibm_fez')>

Możesz transpilować swój Circuit do wybranego Backend, tworząc menedżer przejść, a następnie uruchamiając menedżer przejść na obwód. Łatwym sposobem tworzenia menedżera przejść jest użycie funkcji generate_preset_pass_manager. Zobacz Transpilowanie z menedżerami przejść, aby uzyskać bardziej szczegółowe wyjaśnienie transpilowania 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)

Output of the previous code cell

Transpilowany Circuit zawiera teraz tylko instrukcje ISA. Wszystkie bramki zostały rozłożone w terminach bramek X\sqrt{X} i rotacji RzR_z oraz bramek CZ.

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 terminach 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(['IIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])

Krok 3: Wykonanie z użyciem prymitywów Qiskit

Jesteś teraz gotowy do uruchomienia swojego Circuit przy użyciu prymitywu Estimator.

Tutaj prześlesz pięć osobnych zadań, zaczynając od braku tłumienia lub łagodzenia błędów, a następnie stopniowo włączając różne opcje tłumienia i łagodzenia błędów dostępne w Qiskit Runtime. Aby uzyskać informacje o opcjach, zapoznaj się z następującymi stronami:

Ponieważ te zadania mogą działać niezależnie od siebie, możesz użyć trybu batch, 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)
estimator.options.environment.job_tags = [
"TUT_CEM_SS"
] # add tag for this small scale job
# 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: Post-przetwarzanie i zwracanie wyników w pożądanym formacie klasycznym

Na koniec możesz przeanalizować dane. Tutaj pobierzesz wyniki zadań, wyodrębnisz z nich zmierzone wartości oczekiwane i wyrysujemy wartości wraz z słupkami błędów 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()

Output of the previous code cell

W tej małej skali trudno dostrzec efekt większości technik łagodzenia błędów, ale ekstrapolacja zerowego szumu daje zauważalną poprawę. Pamiętaj jednak, że ta poprawa nie jest darmowa, ponieważ wynik ZNE ma również większy słupek błędu.

Przykład sprzętowy na dużą skalę

Podczas opracowywania eksperymentu warto zacząć od małego Circuit, aby ułatwić wizualizacje i symulacje. Teraz, gdy opracowałeś i przetestowałeś nasz przepływ pracy na 10-qubitowym Circuit, możesz go skalować do 50 qubitów. Poniższy blok kodu powtarza wszystkie kroki z tego przewodnika, ale teraz stosuje je do 50-qubitowego Circuit.

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)
estimator.options.environment.job_tags = [
"TUT_CEM_LS"
] # add tag for this large scale job
# 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()

Output of the previous code cell

Porównując wyniki 50-qubitowe z wcześniejszymi wynikami 10-qubitowymi, możesz zauważyć następujące rzeczy (twoje wyniki mogą się różnić między uruchomieniami):

  • Wszystkie eksperymenty dają wyniki bliższe wartości idealnej, a wszystkie słupki błędów są mniejsze.
  • Dodanie dynamicznego odsprzęgania może pogorszyć wydajność w porównaniu z przypadkiem bez łagodzenia. Nie jest to zaskakujące, ponieważ Circuit jest bardzo gęsty. Dynamiczne odsprzęganie jest przede wszystkim przydatne, gdy w Circuit są duże luki, podczas których qubity siedzą bezczynnie bez aplikowanych bramek. Gdy te luki nie są obecne, dynamiczne odsprzęganie nie jest skuteczne i może faktycznie pogorszyć wydajność z powodu błędów w samych impulsach dynamicznego odsprzęgania. 10-qubitowy Circuit mógł być zbyt mały, abyśmy mogli zaobserwować ten efekt.
  • Przy ekstrapolacji zerowego szumu wynik jest bardzo bliski wartości idealnej. To demonstruje moc ZNE.

Kolejne kroki

Rekomendacje

Jeśli uznałeś tę pracę za interesującą, możesz zainteresować się następującymi materiałami na temat dodatkowych technik łagodzenia błędów i tłumienia błędów, które nie zostały wspomniane w tym samouczku: