Mitygacja błędów odczytu dla prymitywu Sampler z użyciem M3
Szacowane użycie: poniżej jednej minuty na procesorze Heron r2 (UWAGA: To jest wyłącznie szacunek. Twój czas wykonania może się różnić.)
Wprowadzenie
W odróżnieniu od prymitywu Estimator, prymityw Sampler nie ma wbudowanego wsparcia dla mitygacji błędów. Kilka metod obsługiwanych przez Estimator jest zaprojektowanych specjalnie dla wartości oczekiwanych i dlatego nie ma zastosowania do prymitywu Sampler. Wyjątkiem jest mitygacja błędów odczytu, która jest wysoce skuteczną metodą mającą zastosowanie również do prymitywu Sampler.
Dodatek M3 do Qiskit implementuje efektywną metodę mitygacji błędów odczytu. Ten samouczek wyjaśnia, jak używać dodatku M3 do Qiskit w celu mitygacji błędów odczytu dla prymitywu Sampler.
Czym jest błąd odczytu?
Bezpośrednio przed pomiarem stan rejestru Qubitów jest opisany superpozycją stanów bazy obliczeniowej lub macierzą gęstości. Pomiar rejestru Qubitów do klasycznego rejestru bitów przebiega dwuetapowo. Najpierw wykonywany jest właściwy pomiar kwantowy. Oznacza to, że stan rejestru Qubitów jest rzutowany na pojedynczy stan bazowy charakteryzowany ciągiem -ek i -ek. Drugi etap polega na odczytaniu bitstringa charakteryzującego ten stan bazowy i zapisaniu go w pamięci klasycznego komputera. Nazywamy ten krok odczytem. Okazuje się, że drugi etap (odczyt) generuje więcej błędów niż pierwszy (rzutowanie na stany bazowe). Ma to sens, gdy przypomnisz sobie, że odczyt wymaga wykrycia mikroskopowego stanu kwantowego i wzmocnienia go do sfery makroskopowej. Rezonator odczytu jest sprzężony z Qubitem (transmonem), dzięki czemu doświadcza bardzo małego przesunięcia częstotliwości. Impuls mikrofalowy jest następnie odbijany od rezonatora, przy czym doświadcza małych zmian swoich charakterystyk. Odbity impuls jest następnie wzmacniany i analizowany. Jest to delikatny proces i podlega szeregowi błędów.
Ważny wniosek jest taki, że choć zarówno pomiar kwantowy, jak i odczyt są obarczone błędami, ten drugi generuje dominujący błąd, zwany błędem odczytu, który jest przedmiotem tego samouczka.
Podstawy teoretyczne
Jeśli próbkowany bitsring (przechowywany w pamięci klasycznej) różni się od bitstringa charakteryzującego rzutowany stan kwantowy, mówimy, że wystąpił błąd odczytu. Obserwuje się, że te błędy są losowe i nieskorelowane między próbkami. Okazało się użyteczne modelowanie błędu odczytu jako zaszumionego kanału klasycznego. To znaczy, dla każdej pary bitstringów i , istnieje stałe prawdopodobieństwo, że prawdziwa wartość zostanie błędnie odczytana jako .
Dokładniej, dla każdej pary bitstringów istnieje (warunkowe) prawdopodobieństwo , że zostanie odczytane, pod warunkiem że prawdziwa wartość to Czyli,
gdzie jest liczbą bitów w rejestrze odczytu. Dla konkretności zakładamy, że jest dziesiętną liczbą całkowitą, której binarna reprezentacja jest bitstringiem etykietującym stany bazy obliczeniowej. Macierz nazywamy macierzą przypisań. Dla stałej prawdziwej wartości suma prawdopodobieństw po wszystkich zaszumionych wynikach musi wynosić . Czyli
Macierz bez ujemnych wpisów spełniająca (1) nazywana jest lewostochastyczną. Macierz lewostochastyczna jest też nazywana kolumnowostochastyczną, ponieważ każda z jej kolumn sumuje się do . Eksperymentalnie wyznaczamy przybliżone wartości każdego elementu , wielokrotnie przygotowując każdy stan bazowy i obliczając częstości wystąpień próbkowanych bitstringów.
Jeśli eksperyment polega na szacowaniu rozkładu prawdopodobieństwa na wyjściowych bitstringach przez wielokrotne próbkowanie, możemy użyć do mitygacji błędu odczytu na poziomie rozkładu. Pierwszym krokiem jest wielokrotne wykonanie interesującego obwodu, tworząc histogram próbkowanych bitstringów. Znormalizowany histogram to zmierzony rozkład prawdopodobieństwa na możliwych bitstringach, który oznaczamy przez . (Szacowane) prawdopodobieństwo próbkowania bitstringa jest równe sumie po wszystkich prawdziwych bitstringach , każdy ważony przez prawdopodobieństwo, że zostanie mylony z . Stwierdzenie to w formie macierzowej brzmi:
gdzie jest prawdziwym rozkładem. Innymi słowy, błąd odczytu ma efekt pomnożenia idealnego rozkładu bitstringów przez macierz przypisań , dając obserwowany rozkład . Zmierzyliśmy i , ale nie mamy bezpośredniego dostępu do . Zasadniczo uzyskamy prawdziwy rozkład bitstringów dla naszego obwodu, rozwiązując równanie (2) dla numerycznie.
Zanim przejdziemy dalej, warto zwrócić uwagę na kilka ważnych cech tego naiwnego podejścia.
- W praktyce równanie (2) nie jest rozwiązywane przez odwrócenie . Procedury algebry liniowej w bibliotekach programistycznych stosują metody bardziej stabilne, dokładne i efektywne.
- Szacując , zakładaliśmy, że wystąpiły wyłącznie błędy odczytu. W szczególności zakładamy, że nie było błędów przygotowania stanu i pomiaru kwantowego — lub przynajmniej że zostały one w inny sposób zmitigowane. W takim stopniu, w jakim jest to dobre założenie, rzeczywiście reprezentuje wyłącznie błąd odczytu. Ale gdy używamy do korekty zmierzonego rozkładu bitstringów, nie czynimy takiego założenia. W rzeczywistości oczekujemy, że interesujący obwód wprowadzi szumy, na przykład błędy bramek. „Prawdziwy" rozkład nadal zawiera efekty wszelkich błędów, które nie zostały w inny sposób zmitigowane.
Ta metoda, choć użyteczna w pewnych okolicznościach, ma kilka ograniczeń.
Zasoby przestrzeni i czasu potrzebne do oszacowania rosną wykładniczo wraz z :
- Szacowanie i jest obarczone błędem statystycznym wynikającym ze skończonego próbkowania. Szum ten można zminimalizować do dowolnie małej wartości kosztem większej liczby shotów (do skali czasowej dryfujących parametrów sprzętu, które skutkują systematycznymi błędami w ). Jednak jeśli nie przyjmuje się żadnych założeń dotyczących bitstringów obserwowanych podczas mitygacji, liczba shotów wymagana do oszacowania rośnie co najmniej wykładniczo wraz z .
- jest macierzą . Gdy , ilość pamięci potrzebna do przechowania jest większa niż pamięć dostępna w mocnym laptopie.
Dalsze ograniczenia to:
- Odtworzony rozkład może mieć jedno lub więcej ujemnych prawdopodobieństw (sumując się nadal do jedności). Jednym rozwiązaniem jest minimalizacja przy ograniczeniu, że każdy element jest nieujemny. Jednak czas wykonania takiej metody jest o rzędy wielkości dłuższy niż bezpośrednie rozwiązanie równania (2).
- Ta procedura mitygacji działa na poziomie rozkładu prawdopodobieństwa bitstringów. W szczególności nie może skorygować błędu w pojedynczym obserwowanym bitstringu.
Dodatek M3 do Qiskit: skalowanie do dłuższych bitstringów
Rozwiązanie równania (2) za pomocą standardowych numerycznych procedur algebry liniowej jest ograniczone do bitstringów o długości nie większej niż około 10 bitów. M3 może jednak obsługiwać znacznie dłuższe bitstringi. Dwie kluczowe właściwości M3, które to umożliwiają, to:
- Korelacje błędów odczytu rzędu trzeciego i wyższego wśród zbiorów bitów są uznawane za pomijalnie małe i są ignorowane. Zasadniczo, kosztem większej liczby shotów, można szacować wyższe korelacje.
- Zamiast konstruować wprost, używamy znacznie mniejszej efektywnej macierzy, która rejestruje prawdopodobieństwa wyłącznie dla bitstringów zebranych podczas konstruowania .
Na wysokim poziomie procedura działa następująco.
Najpierw konstruujemy bloki budulcowe, z których możemy zbudować uproszczony, efektywny opis . Następnie wielokrotnie uruchamiamy interesujący obwód i zbieramy bitstringi, których używamy do konstruowania zarówno , jak i — z pomocą bloków budulcowych — efektywnego .
Dokładniej:
-
Jednokubitowe macierze przypisań są szacowane dla każdego Qubitu. W tym celu wielokrotnie przygotowujemy rejestr Qubitów w stanie zerowym , a następnie w stanie jedynkowym , i rejestrujemy prawdopodobieństwo, że każdy Qubit zostanie odczytany niepoprawnie.
-
Korelacje rzędu trzeciego i wyższe są uznawane za pomijalnie małe i są ignorowane.
Zamiast tego konstruujemy macierzy przypisań dla pojedynczych Qubitów oraz macierzy przypisań dla par Qubitów. Te jedno- i dwukubitowe macierze przypisań są przechowywane do późniejszego użycia.
-
Po wielokrotnym próbkowaniu obwodu w celu konstruowania , konstruujemy efektywne przybliżenie , używając wyłącznie bitstringów próbkowanych podczas konstruowania . Ta efektywna macierz jest budowana z użyciem opisanych wcześniej macierzy jedno- i dwukubitowych. Wymiar liniowy tej macierzy wynosi co najwyżej rzędu liczby shotów użytych podczas konstruowania , co jest znacznie mniejsze niż wymiar pełnej macierzy przypisań .
Szczegóły techniczne dotyczące M3 można znaleźć w artykule Scalable Mitigation of Measurement Errors on Quantum Computers.
Zastosowanie M3 do algorytmu kwantowego
Zastosujemy mitygację odczytu M3 do problemu ukrytego przesunięcia. Problem ukrytego przesunięcia i ściśle z nim powiązane problemy, takie jak problem ukrytej podgrupy, zostały pierwotnie opracowane w kontekście odporności na błędy (a dokładniej, zanim udowodniono możliwość odpornych na błędy QPU!). Są jednak również badane na dostępnych procesorach. Przykład algorytmicznego wykładniczego przyspieszenia uzyskanego dla wariantu problemu ukrytego przesunięcia na 127-kubitowych QPU IBM® można znaleźć w tym artykule (wersja arXiv).
W poniższym tekście całe działania arytmetyczne są boolowskie. To znaczy, dla , dodawanie jest funkcją logiczną XOR. Ponadto mnożenie (lub ) jest funkcją logiczną AND. Dla , jest zdefiniowane przez bitowe zastosowanie XOR. Iloczyn skalarny jest zdefiniowany przez .
Operator Hadamarda i transformata Fouriera
W implementacji algorytmów kwantowych bardzo powszechne jest używanie operatora Hadamarda jako transformaty Fouriera. Stany bazy obliczeniowej są niekiedy nazywane stanami klasycznymi. Pozostają one w relacji jeden do jednego z klasycznymi bitstringami. Operator Hadamarda -Qubitowy na stanach klasycznych można traktować jako transformatę Fouriera na boolowskiej hiperkostce:
Rozważmy stan odpowiadający stałemu bitstringowi . Stosując i używając , widzimy, że transformatę Fouriera można zapisać jako
Operator Hadamarda jest swoją własną odwrotnością, czyli . Tak więc odwrotna transformata Fouriera jest tym samym operatorem, . Wprost mamy:
Problem ukrytego przesunięcia
Rozważamy prosty przykład problemu ukrytego przesunięcia. Problem polega na identyfikacji stałego przesunięcia na wejściu funkcji. Funkcją, którą rozważamy, jest iloczyn skalarny. Jest to najprostszy element dużej klasy funkcji dopuszczających kwantowe przyspieszenie dla problemu ukrytego przesunięcia za pomocą technik podobnych do tych przedstawionych poniżej.
Niech będą bitstringami długości . Definiujemy przez
Niech będą stałymi bitstringami długości . Definiujemy ponadto przez
gdzie i są (ukrytymi) parametrami. Dane są nam dwie czarne skrzynki: jedna implementująca , a druga . Zakładamy, że wiemy, że obliczają funkcje zdefiniowane powyżej, z tym że nie znamy ani , ani . Celem jest wyznaczenie ukrytych bitstringów (przesunięć) i przez wykonywanie zapytań do i . Oczywiste jest, że jeśli rozgrywamy tę grę klasycznie, potrzebujemy zapytań, aby wyznaczyć i . Na przykład możemy zapytać o wszystkie pary ciągów, takie że jeden element pary to same zera, a drugi ma dokładnie jeden element równy . W każdym zapytaniu poznajemy jeden element lub . Jednak zobaczymy, że jeśli czarne skrzynki są zaimplementowane jako obwody kwantowe, możemy wyznaczyć i za pomocą jednego zapytania do każdego z i .
W kontekście złożoności algorytmicznej czarna skrzynka jest nazywana wyrocznią. Oprócz bycia nieprzezroczystą, wyrocznia ma tę właściwość, że zużywa dane wejściowe i zwraca dane wyjściowe natychmiastowo, nie dodając nic do budżetu złożoności algorytmu, w którym jest osadzona. W rzeczywistości w rozpatrywanym przypadku wyrocznie implementujące i okazują się efektywne.
Obwody kwantowe dla i
Potrzebujemy następujących składników, aby zaimplementować i jako obwody kwantowe.
Dla jednobitowych stanów klasycznych , z , bramka controlled- może być zapisana jako
Będziemy operować bramkami CZ, jedną na , jedną na , i tak dalej, aż do . Ten operator nazywamy .
jest kwantową wersją :
Musimy też zaimplementować przesunięcie bitstringa. Oznaczamy operator na rejestrze przez jako i analogicznie na rejestrze przez . Operatory te stosują wszędzie tam, gdzie pojedynczy bit wynosi , i tożsamość wszędzie tam, gdzie wynosi . Wtedy mamy
Druga czarna skrzynka jest zaimplementowana przez unitarną , daną przez
Aby to zobaczyć, stosujemy operatory od prawej do lewej do stanu . Najpierw
Następnie,
Na koniec,
co jest rzeczywiście kwantową wersją .
Algorytm ukrytego przesunięcia
Teraz łączymy elementy, aby rozwiązać problem ukrytego przesunięcia. Zaczynamy od zastosowania Hadamardów do rejestrów zainicjalizowanych w stanie zerowym.
Następnie pytamy wyrocznię , aby uzyskać
W ostatniej linii pominęliśmy stały globalny czynnik fazowy i oznaczamy równość z dokładnością do fazy przez . Następnie zastosowanie wyroczni wprowadza kolejny czynnik , który anuluje już obecny. Mamy wtedy:
Ostatnim krokiem jest zastosowanie odwrotnej transformaty Fouriera, , dając w wyniku
Obwód jest ukończony. W przypadku braku szumu próbkowanie rejestrów kwantowych zwróci bitstringi z prawdopodobieństwem .
Boolowski iloczyn skalarny jest przykładem tak zwanych funkcji giętych. Nie będziemy tu definiować funkcji giętych, lecz jedynie zaznaczymy, że „są maksymalnie odporne na ataki, które starają się wykorzystać zależność wyjść od pewnej liniowej podprzestrzeni wejść." Cytat pochodzi z artykułu Quantum algorithms for highly non-linear Boolean functions, który podaje efektywne algorytmy ukrytego przesunięcia dla kilku klas funkcji giętych. Algorytm z tego samouczka pojawia się w sekcji 3.1 artykułu.
W bardziej ogólnym przypadku obwód do wyznaczania ukrytego przesunięcia to
W ogólnym przypadku i są funkcjami jednej zmiennej. Nasz przykład iloczynu skalarnego ma tę postać, jeśli pozwolimy , gdzie jest konkatenacją i , a jest konkatenacją i . Ogólny przypadek wymaga dokładnie dwóch wyroczni: jednej dla i jednej dla , gdzie ta ostatnia jest funkcją zwaną dualną do funkcji giętej . Funkcja iloczynu skalarnego ma właściwość samoodwrotności .
W naszym obwodzie dla ukrytego przesunięcia na iloczynie skalarnym pominęliśmy środkową warstwę Hadamardów, która pojawia się w obwodzie dla ogólnego przypadku. Choć w ogólnym przypadku ta warstwa jest konieczna, zaoszczędziliśmy trochę głębokości, pomijając ją, kosztem odrobiny dodatkowego przetwarzania, ponieważ wyjściem jest zamiast pożądanego .
Wymagania
Przed rozpoczęciem tego samouczka upewnij się, że masz zainstalowane następujące elementy:
- Qiskit SDK v2.1 lub nowszy, z obsługą wizualizacji
- Qiskit Runtime v0.41 lub nowszy (
pip install qiskit-ibm-runtime) - Dodatek M3 do Qiskit v3.0 (
pip install mthree)
Konfiguracja
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib mthree qiskit qiskit-ibm-runtime
from collections.abc import Iterator, Sequence
from random import Random
from qiskit.circuit import (
CircuitInstruction,
QuantumCircuit,
QuantumRegister,
Qubit,
)
from qiskit.circuit.library import CZGate, HGate, XGate
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService
import timeit
import matplotlib.pyplot as plt
from qiskit_ibm_runtime import SamplerV2 as Sampler
import mthree
Krok 1: Odwzorowanie klasycznych danych wejściowych na problem kwantowy
Najpierw piszemy funkcje implementujące problem ukrytego przesunięcia jako QuantumCircuit.
def apply_hadamards(qubits: Sequence[Qubit]) -> Iterator[CircuitInstruction]:
"""Apply a Hadamard gate to every qubit."""
for q in qubits:
yield CircuitInstruction(HGate(), [q], [])
def apply_shift(
qubits: Sequence[Qubit], shift: int
) -> Iterator[CircuitInstruction]:
"""Apply X gates where the bits of the shift are equal to 1."""
for i, q in zip(range(shift.bit_length()), qubits):
if shift >> i & 1:
yield CircuitInstruction(XGate(), [q], [])
def oracle_f(qubits: Sequence[Qubit]) -> Iterator[CircuitInstruction]:
"""Apply the f oracle."""
for i in range(0, len(qubits) - 1, 2):
yield CircuitInstruction(CZGate(), [qubits[i], qubits[i + 1]])
def oracle_g(
qubits: Sequence[Qubit], shift: int
) -> Iterator[CircuitInstruction]:
"""Apply the g oracle."""
yield from apply_shift(qubits, shift)
yield from oracle_f(qubits)
yield from apply_shift(qubits, shift)
def determine_hidden_shift(
qubits: Sequence[Qubit], shift: int
) -> Iterator[CircuitInstruction]:
"""Determine the hidden shift."""
yield from apply_hadamards(qubits)
yield from oracle_g(qubits, shift)
# We omit this layer in exchange for post processing
# yield from apply_hadamards(qubits)
yield from oracle_f(qubits)
yield from apply_hadamards(qubits)
def run_hidden_shift_circuit(n_qubits, rng):
hidden_shift = rng.getrandbits(n_qubits)
qubits = QuantumRegister(n_qubits, name="q")
circuit = QuantumCircuit.from_instructions(
determine_hidden_shift(qubits, hidden_shift), qubits=qubits
)
circuit.measure_all()
# Format the hidden shift as a string.
hidden_shift_string = format(hidden_shift, f"0{n_qubits}b")
return (circuit, hidden_shift, hidden_shift_string)
def display_circuit(circuit):
return circuit.remove_final_measurements(inplace=False).draw(
"mpl", idle_wires=False, scale=0.5, fold=-1
)
Zaczniemy od małego przykładu:
n_qubits = 6
random_seed = 12345
rng = Random(random_seed)
circuit, hidden_shift, hidden_shift_string = run_hidden_shift_circuit(
n_qubits, rng
)
print(f"Hidden shift string {hidden_shift_string}")
display_circuit(circuit)
Hidden shift string 011010
Krok 2: Optymalizacja obwodów do wykonania na sprzęcie kwantowym
job_tags = [
f"shift {hidden_shift_string}",
f"n_qubits {n_qubits}",
f"seed = {random_seed}",
]
job_tags
['shift 011010', 'n_qubits 6', 'seed = 12345']
# Uncomment this to run the circuits on a quantum computer on IBMCloud.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=100
)
# from qiskit_ibm_runtime.fake_provider import FakeMelbourneV2
# backend = FakeMelbourneV2()
# backend.refresh(service)
print(f"Using backend {backend.name}")
def get_isa_circuit(circuit, backend):
pass_manager = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=1234
)
isa_circuit = pass_manager.run(circuit)
return isa_circuit
isa_circuit = get_isa_circuit(circuit, backend)
display_circuit(isa_circuit)
Using backend ibm_kingston
Krok 3: Wykonanie obwodów za pomocą prymitywów Qiskit
# submit job for solving the hidden shift problem using the Sampler primitive
NUM_SHOTS = 50_000
def run_sampler(backend, isa_circuit, num_shots):
sampler = Sampler(mode=backend)
sampler.options.environment.job_tags
pubs = [(isa_circuit, None, NUM_SHOTS)]
job = sampler.run(pubs)
return job
def setup_mthree_mitigation(isa_circuit, backend):
# retrieve the final qubit mapping so mthree knows which qubits to calibrate
qubit_mapping = mthree.utils.final_measurement_mapping(isa_circuit)
# submit jobs for readout error calibration
mit = mthree.M3Mitigation(backend)
mit.cals_from_system(qubit_mapping, rep_delay=None)
return mit, qubit_mapping
job = run_sampler(backend, isa_circuit, NUM_SHOTS)
mit, qubit_mapping = setup_mthree_mitigation(isa_circuit, backend)
Krok 4: Post-processing i zwracanie wyników w formacie klasycznym
W omówieniu teoretycznym powyżej ustaliliśmy, że dla wejścia oczekujemy wyjścia .
Dodatkowym utrudnieniem jest to, że w celu uzyskania prostszego (przed transpilacją) obwodu wstawiliśmy wymagane bramki CZ między
sąsiadującymi parami qubitów. Odpowiada to przeplataniu ciągów bitów i jako .
Wyjściowy ciąg będzie przeplatany w podobny sposób: . Funkcja unscramble poniżej
przekształca wyjściowy ciąg z na , dzięki czemu ciągi wejściowe i wyjściowe można bezpośrednio porównać.
# retrieve bitstring counts
def get_bitstring_counts(job):
result = job.result()
pub_result = result[0]
counts = pub_result.data.meas.get_counts()
return counts, pub_result
counts, pub_result = get_bitstring_counts(job)
Odległość Hamminga między dwoma ciągami bitów to liczba pozycji, na których bity się różnią.
def hamming_distance(s1, s2):
weight = 0
for c1, c2 in zip(s1, s2):
(c1, c2) = (int(c1), int(c2))
if (c1 == 1 and c2 == 1) or (c1 == 0 and c2 == 0):
weight += 1
return weight
# Replace string of form a1b1a2b2... with b1a1b2a1...
# That is, reverse order of successive pairs of bits.
def unscramble(bitstring):
ps = [bitstring[i : i + 2][::-1] for i in range(0, len(bitstring), 2)]
return "".join(ps)
def find_hidden_shift_bitstring(counts, hidden_shift_string):
# convert counts to probabilities
probs = {
unscramble(bitstring): count / NUM_SHOTS
for bitstring, count in counts.items()
}
# Retrieve the most probable bitstring.
most_probable = max(probs, key=lambda x: probs[x])
print(f"Expected hidden shift string: {hidden_shift_string}")
if most_probable == hidden_shift_string:
print("Most probable bitstring matches hidden shift 😊.")
else:
print("Most probable bitstring didn't match hidden shift ☹️.")
print("Top 10 bitstrings and their probabilities:")
display(
{
k: (v, hamming_distance(hidden_shift_string, k))
for k, v in sorted(
probs.items(), key=lambda x: x[1], reverse=True
)[:10]
}
)
return probs, most_probable
probs, most_probable = find_hidden_shift_bitstring(
counts, hidden_shift_string
)
Expected hidden shift string: 011010
Most probable bitstring matches hidden shift 😊.
Top 10 bitstrings and their probabilities:
{'011010': (0.9743, 6),
'001010': (0.00812, 5),
'010010': (0.0063, 5),
'011000': (0.00554, 5),
'011011': (0.00492, 5),
'011110': (0.00044, 5),
'001000': (0.00012, 4),
'010000': (8e-05, 4),
'001011': (6e-05, 4),
'000010': (6e-05, 4)}
Zanotujmy prawdopodobieństwo najbardziej prawdopodobnego ciągu bitów przed zastosowaniem korekcji błędów odczytu za pomocą M3.
max_probability_before_M3 = probs[most_probable]
max_probability_before_M3
0.9743
Teraz stosujemy korekcję odczytu wyuczoną przez M3 do zliczonych wyników.
Funkcja apply_corrections zwraca quasi-rozkład prawdopodobieństwa. Jest to lista obiektów float sumujących się do . Jednak niektóre wartości mogą być ujemne.
def perform_mitigation(mit, counts, qubit_mapping):
# mitigate readout error
quasis = mit.apply_correction(counts, qubit_mapping)
# print results
most_probable_after_m3 = unscramble(max(quasis, key=lambda x: quasis[x]))
is_hidden_shift_identified = most_probable_after_m3 == hidden_shift_string
if is_hidden_shift_identified:
print("Most probable bitstring matches hidden shift 😊.")
else:
print("Most probable bitstring didn't match hidden shift ☹️.")
print("Top 10 bitstrings and their quasi-probabilities:")
topten = {
unscramble(k): f"{v:.2e}"
for k, v in sorted(quasis.items(), key=lambda x: x[1], reverse=True)[
:10
]
}
max_probability_after_M3 = float(topten[most_probable_after_m3])
display(topten)
return max_probability_after_M3, is_hidden_shift_identified
print(f"Expected hidden shift string: {hidden_shift_string}")
max_probability_after_M3, is_hidden_shift_identified = perform_mitigation(
mit, counts, qubit_mapping
)
Expected hidden shift string: 011010
Most probable bitstring matches hidden shift 😊.
Top 10 bitstrings and their quasi-probabilities:
{'011010': '1.01e+00',
'001010': '8.75e-04',
'001000': '7.38e-05',
'010000': '4.51e-05',
'111000': '2.18e-05',
'001011': '1.74e-05',
'000010': '6.42e-06',
'011001': '-7.18e-06',
'011000': '-4.53e-04',
'010010': '-1.28e-03'}
Porównanie identyfikacji ukrytego ciągu przesunięcia przed i po zastosowaniu korekcji M3
def compare_before_and_after_M3(
max_probability_before_M3,
max_probability_after_M3,
is_hidden_shift_identified,
):
is_probability_improved = (
max_probability_after_M3 > max_probability_before_M3
)
print(f"Most probable probability before M3: {max_probability_before_M3}")
print(f"Most probable probability after M3: {max_probability_after_M3}")
if is_hidden_shift_identified and is_probability_improved:
print("Readout error mitigation effective! 😊")
else:
print("Readout error mitigation not effective. ☹️")
compare_before_and_after_M3(
max_probability_before_M3,
max_probability_after_M3,
is_hidden_shift_identified,
)
Most probable probability before M3: 0.9743
Most probable probability after M3: 1.01
Readout error mitigation effective! 😊
Wykres skalowania czasu CPU wymaganego przez M3 względem liczby strzałów
# Collect samples for numbers of shots varying from 5000 to 25000.
shots_range = range(5000, NUM_SHOTS + 1, 2500)
times = []
for shots in shots_range:
print(f"Applying M3 correction to {shots} shots...")
t0 = timeit.default_timer()
_ = mit.apply_correction(
pub_result.data.meas.slice_shots(range(shots)).get_counts(),
qubit_mapping,
)
t1 = timeit.default_timer()
print(f"\tDone in {t1 - t0} seconds.")
times.append(t1 - t0)
fig, ax = plt.subplots()
ax.plot(shots_range, times, "o--")
ax.set_xlabel("Shots")
ax.set_ylabel("Time (s)")
ax.set_title("Time to apply M3 correction")
Applying M3 correction to 5000 shots...
Done in 0.003321983851492405 seconds.
Applying M3 correction to 7500 shots...
Done in 0.004425413906574249 seconds.
Applying M3 correction to 10000 shots...
Done in 0.006366567220538855 seconds.
Applying M3 correction to 12500 shots...
Done in 0.0071477219462394714 seconds.
Applying M3 correction to 15000 shots...
Done in 0.00860048783943057 seconds.
Applying M3 correction to 17500 shots...
Done in 0.010026784148067236 seconds.
Applying M3 correction to 20000 shots...
Done in 0.011459112167358398 seconds.
Applying M3 correction to 22500 shots...
Done in 0.012727141845971346 seconds.
Applying M3 correction to 25000 shots...
Done in 0.01406092382967472 seconds.
Applying M3 correction to 27500 shots...
Done in 0.01546052098274231 seconds.
Applying M3 correction to 30000 shots...
Done in 0.016769016161561012 seconds.
Applying M3 correction to 32500 shots...
Done in 0.019537431187927723 seconds.
Applying M3 correction to 35000 shots...
Done in 0.019739801064133644 seconds.
Applying M3 correction to 37500 shots...
Done in 0.021093040239065886 seconds.
Applying M3 correction to 40000 shots...
Done in 0.022840639110654593 seconds.
Applying M3 correction to 42500 shots...
Done in 0.023974396288394928 seconds.
Applying M3 correction to 45000 shots...
Done in 0.026412792038172483 seconds.
Applying M3 correction to 47500 shots...
Done in 0.026364430785179138 seconds.
Applying M3 correction to 50000 shots...
Done in 0.02820305060595274 seconds.
Text(0.5, 1.0, 'Time to apply M3 correction')
Interpretacja wykresu
Powyższy wykres pokazuje, że czas potrzebny do zastosowania korekcji M3 skaluje się liniowo względem liczby strzałów.
Skalowanie w górę
n_qubits = 80
rng = Random(12345)
circuit, hidden_shift, hidden_shift_string = run_hidden_shift_circuit(
n_qubits, rng
)
print(f"Hidden shift string {hidden_shift_string}")
Hidden shift string 00000010100110101011101110010001010000110011101001101010101001111001100110000111
isa_circuit = get_isa_circuit(circuit, backend)
job = run_sampler(backend, isa_circuit, NUM_SHOTS)
mit, qubit_mapping = setup_mthree_mitigation(isa_circuit, backend)
counts, pub_result = get_bitstring_counts(job)
probs, most_probable = find_hidden_shift_bitstring(
counts, hidden_shift_string
)
Expected hidden shift string: 00000010100110101011101110010001010000110011101001101010101001111001100110000111
Most probable bitstring matches hidden shift 😊.
Top 10 bitstrings and their probabilities:
{'00000010100110101011101110010001010000110011101001101010101001111001100110000111': (0.50402,
80),
'00000010100110101011101110010001010000110011100001101010101001111001100110000111': (0.0396,
79),
'00000010100110101011101110010001010000110011101001101010101001111001100100000111': (0.0323,
79),
'00000010100110101011101110010001010000110011101001101010101001101001100110000111': (0.01936,
79),
'00000010100110101011101110010011010000110011101001101010101001111001100110000111': (0.01432,
79),
'00000010100110101011101110010001010000110011101001101010101001011001100110000111': (0.0101,
79),
'00000010100110101011101110010001010000110011101001101010101001110001100110000111': (0.00924,
79),
'00000010100110101011101110010001010000010011101001101010101001111001100110000111': (0.00908,
79),
'00000010100110101011100110010001010000110011101001101010101001111001100110000111': (0.00888,
79),
'00000010100110101011101110010001010000110011101001100010101001111001100110000111': (0.0082,
79)}
Widzimy, że poprawny ukryty ciąg przesunięcia został znaleziony. Co więcej, dziewięć następnych najbardziej prawdopodobnych ciągów bitów różni się od niego tylko na jednej pozycji. Zanotujmy najbardziej prawdopodobne prawdopodobieństwo:
max_probability_before_M3 = probs[most_probable]
max_probability_before_M3
0.50402
print(f"Expected hidden shift string: {hidden_shift_string}")
max_probability_after_M3, is_hidden_shift_identified = perform_mitigation(
mit, counts, qubit_mapping
)
Expected hidden shift string: 00000010100110101011101110010001010000110011101001101010101001111001100110000111
Most probable bitstring matches hidden shift 😊.
Top 10 bitstrings and their quasi-probabilities:
{'00000010100110101011101110010001010000110011101001101010101001111001100110000111': '9.85e-01',
'00000010100110101011101110010001010000110011100001101010101001111001100110000111': '6.84e-03',
'00000010100110101011100110010001010000110011101001101010101001111001100110000111': '3.87e-03',
'00000010100110101011101110010011010000110011101001101010101001111001100110000111': '3.42e-03',
'00000010100110101011101110010001010000110011101001101010101001111001100100000111': '3.30e-03',
'00000010100110101011101110010001010000110011101001101010101001110001100110000111': '3.28e-03',
'00000010100010101011101110010001010000110011101001101010101001111001100110000111': '2.62e-03',
'00000010100110101011101110010001010000110011101001101010101001101001100110000111': '2.43e-03',
'00000010100110101011101110010000010000110011101001101010101001111001100110000111': '1.73e-03',
'00000010100110101011101110010001010000110011101001101010101001111001000110000111': '1.63e-03'}
compare_before_and_after_M3(
max_probability_before_M3,
max_probability_after_M3,
is_hidden_shift_identified,
)
Most probable probability before M3: 0.54348
Most probable probability after M3: 0.99
Readout error mitigation effective! 😊
Wyniki pokazują, że błąd odczytu był dominującym źródłem błędu, a mitygacja M3 okazała się skuteczna.