Implementacja w Qiskit
W tej lekcji zaimplementujemy niektóre z koncepcji z lekcji o splątaniu w działaniu, używając Qiskit.
# Added by doQumentation — required packages for this notebook
!pip install -q qiskit qiskit-aer
from qiskit import __version__
print(__version__)
2.1.1
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit_aer import AerSimulator
from qiskit.visualization import plot_histogram, array_to_latex
from qiskit.result import marginal_distribution
from qiskit.circuit.library import UGate
from math import pi
import random
Oto implementacja protokołu teleportacji w postaci obwodu kwantowego.
qubit = QuantumRegister(1, "Q")
ebit0 = QuantumRegister(1, "A")
ebit1 = QuantumRegister(1, "B")
a = ClassicalRegister(1, "a")
b = ClassicalRegister(1, "b")
protocol = QuantumCircuit(qubit, ebit0, ebit1, a, b)
# Prepare ebit used for teleportation
protocol.h(ebit0)
protocol.cx(ebit0, ebit1)
protocol.barrier()
# Alice's operations
protocol.cx(qubit, ebit0)
protocol.h(qubit)
protocol.barrier()
# Alice measures and sends classical bits to Bob
protocol.measure(ebit0, a)
protocol.measure(qubit, b)
protocol.barrier()
# Bob uses the classical bits to conditionally apply gates
with protocol.if_test((a, 1)):
protocol.x(ebit1)
with protocol.if_test((b, 1)):
protocol.z(ebit1)
display(protocol.draw(output="mpl"))
Obwód wykorzystuje kilka funkcji Qiskit, których nie widzieliśmy jeszcze w poprzednich lekcjach, w tym funkcje barrier i if_test.
Funkcja barrier tworzy wizualne rozdzielenie, dzięki czemu diagram obwodu jest bardziej czytelny, a także uniemożliwia Qiskit przeprowadzanie różnych uproszczeń i optymalizacji w poprzek bariery podczas kompilacji, gdy obwody są uruchamiane na prawdziwym sprzęcie.
Funkcja if_test stosuje operację warunkowo w zależności od bitu lub rejestru klasycznego.
Obwód najpierw inicjalizuje w stanie (co nie jest częścią samego protokołu), następnie wykonuje operacje Alicji, potem jej pomiary, a na koniec operacje Boba. Aby przetestować, czy protokół działa poprawnie, zastosujemy losowo wygenerowaną bramkę jednokubitową do zainicjalizowanego stanu kubitu , aby otrzymać losowy wektor stanu kwantowego, który zostanie teleportowany. Stosując odwrotność (czyli transpozycję sprzężoną) tej bramki do po wykonaniu protokołu, możemy zweryfikować, że stan został teleportowany, mierząc, czy powrócił do stanu .
Najpierw losowo wybierzemy unitarną bramkę kubitową.
random_gate = UGate(
theta=random.random() * 2 * pi,
phi=random.random() * 2 * pi,
lam=random.random() * 2 * pi,
)
display(array_to_latex(random_gate.to_matrix()))
Teraz utworzymy nowy obwód testowy, który najpierw stosuje naszą losową bramkę do , następnie uruchamia obwód teleportacji, a na koniec stosuje odwrotność naszej losowej bramki do kubitu i wykonuje pomiar. Wynik powinien być z pewnością.
# Create a new circuit including the same bits and qubits used in the
# teleportation protocol.
test = QuantumCircuit(qubit, ebit0, ebit1, a, b)
# Start with the randomly selected gate on Q
test.append(random_gate, qubit)
test.barrier()
# Append the entire teleportation protocol from above.
test = test.compose(protocol)
test.barrier()
# Finally, apply the inverse of the random unitary to B and measure.
test.append(random_gate.inverse(), ebit1)
result = ClassicalRegister(1, "Result")
test.add_register(result)
test.measure(ebit1, result)
display(test.draw(output="mpl"))
Na koniec uruchommy symulator Aer na tym obwodzie i narysujmy histogram wyników. Zobaczymy statystyki dla wszystkich trzech bitów klasycznych: dolny/najbardziej lewy bit powinien zawsze wynosić , co wskazuje, że kubit został pomyślnie teleportowany do , podczas gdy pozostałe dwa bity powinny być w przybliżeniu rozłożone równomiernie.
result = AerSimulator().run(test).result()
statistics = result.get_counts()
display(plot_histogram(statistics))
Możemy również filtrować statystyki, aby skupić się wyłącznie na kubicie wyniku testu, jeśli chcemy, w ten sposób:
filtered_statistics = marginal_distribution(statistics, [2])
display(plot_histogram(filtered_statistics))
Superkodowanie
Superkodowanie to protokół, który w pewnym sensie osiąga cel komplementarny do teleportacji. Zamiast umożliwiać przesłanie jednego kubitu przy użyciu dwóch klasycznych bitów komunikacji (kosztem jednego e-bitu splątania), pozwala na przesłanie dwóch klasycznych bitów przy użyciu jednego kubitu komunikacji kwantowej (ponownie kosztem jednego e-bitu splątania).
Mówiąc bardziej szczegółowo, mamy nadawcę (Alicję) oraz odbiorcę (Boba), którzy dzielą jeden e-bit splątania. Zgodnie z konwencjami przyjętymi w tej lekcji oznacza to, że Alicja ma kubit , Bob ma kubit , a wspólnie para jest w stanie . Alicja chce przesłać Bobowi dwa klasyczne bity, które oznaczymy jako i , i osiągnie to, wysyłając mu jeden kubit.
Rozsądne jest uznanie tego osiągnięcia za mniej interesujące niż to, które umożliwia teleportacja. Wysyłanie kubitów będzie prawdopodobnie o wiele trudniejsze niż wysyłanie klasycznych bitów w dającej się przewidzieć przyszłości, więc wymiana jednego kubitu komunikacji kwantowej na dwa bity komunikacji klasycznej, na dodatek kosztem e-bitu, wydaje się niewiele warta. Nie oznacza to jednak, że superkodowanie nie jest interesujące, ponieważ z pewnością jest.
Pasując do tematu lekcji, jednym z powodów, dla których superkodowanie jest interesujące, jest to, że demonstruje ono konkretne i (w kontekście teorii informacji) dość uderzające wykorzystanie splątania. Słynne twierdzenie w teorii informacji kwantowej, znane jako twierdzenie Holevo, implikuje, że bez użycia współdzielonego stanu splątanego niemożliwe jest przesłanie więcej niż jednego bitu klasycznej informacji przez wysłanie pojedynczego kubitu. (Twierdzenie Holevo jest ogólniejsze niż to. Jego precyzyjne sformułowanie jest techniczne i wymaga wyjaśnienia, ale jest to jedna z jego konsekwencji.) Tak więc poprzez superkodowanie współdzielone splątanie efektywnie pozwala na podwojenie klasycznej pojemności informacyjnej wysyłanych kubitów.
Protokół
Poniższy diagram obwodu kwantowego opisuje protokół superkodowania:

Słownie, oto co robi Alicja:
-
Jeśli , Alicja wykonuje bramkę na swoim kubicie (a jeśli , nie wykonuje).
-
Jeśli , Alicja wykonuje bramkę na swoim kubicie (a jeśli , nie wykonuje).
Następnie Alicja wysyła swój kubit do Boba.
Kiedy Bob otrzymuje kubit , najpierw wykonuje bramkę controlled-NOT, gdzie jest bramką sterującą, a jest bramką celu, a następnie stosuje bramkę Hadamard na . Potem mierzy , aby uzyskać , oraz , aby uzyskać , w obu przypadkach dokonując pomiarów w bazie standardowej.
Analiza
Idea stojąca za tym protokołem jest prosta: Alicja efektywnie wybiera, który stan Bella chciałaby współdzielić z Bobem, wysyła Bobowi swój kubit, a Bob dokonuje pomiaru, aby określić, który stan Bella wybrała Alicja.
Oznacza to, że początkowo współdzielą stan a w zależności od bitów i Alicja albo pozostawia ten stan bez zmian, albo przekształca go w jeden z pozostałych stanów Bella, stosując lub na swoim kubicie
Działania Boba mają następujące efekty na czterech stanach Bella:
Można to sprawdzić bezpośrednio, obliczając wyniki operacji Boba na tych stanach pojedynczo.
Tak więc, gdy Bob dokonuje pomiarów, jest w stanie określić, który stan Bella wybrała Alicja. Weryfikacja, że protokół działa poprawnie, sprowadza się do sprawdzenia każdego przypadku:
-
Jeśli , to stan w chwili, gdy Bob otrzymuje , to . Przekształca ten stan w i uzyskuje .
-
Jeśli , to stan w chwili, gdy Bob otrzymuje , to . Przekształca ten stan w i uzyskuje .
-
Jeśli , to stan w chwili, gdy Bob otrzymuje , to . Przekształca ten stan w i uzyskuje .
-
Jeśli , to stan w chwili, gdy Bob otrzymuje , to . Przekształca ten stan w i uzyskuje . (Czynnik fazowy minus jeden nie ma tu żadnego wpływu.)
Implementacja superkodowania
Oto prosta implementacja superkodowania, w której specyfikujemy sam obwód w zależności od bitów, które mają być przesłane. Najpierw wybierzemy dwa bity do przesłania. (Później wybierzemy je losowo, ale na razie dokonamy arbitralnego wyboru.)
c = "1"
d = "0"
Teraz odpowiednio zbudujemy obwód. Pozwolimy Qiskit używać domyślnych nazw dla kubitów: dla górnego kubitu i dla dolnego.
protocol = QuantumCircuit(2)
# Prepare ebit used for superdense coding
protocol.h(0)
protocol.cx(0, 1)
protocol.barrier()
# Alice's operations
if d == "1":
protocol.z(0)
if c == "1":
protocol.x(0)
protocol.barrier()
# Bob's actions
protocol.cx(0, 1)
protocol.h(0)
protocol.measure_all()
display(protocol.draw(output="mpl"))
Niewiele tu nowego, z wyjątkiem funkcji measure_all, która mierzy wszystkie kubity i umieszcza wyniki w pojedynczym rejestrze klasycznym (mającym zatem w tym przypadku dwa bity).
Uruchomienie symulatora Aer daje oczekiwany wynik.
result = AerSimulator().run(protocol).result()
statistics = result.get_counts()
for outcome, frequency in statistics.items():
print(f"Measured {outcome} with frequency {frequency}")
display(plot_histogram(statistics))
Measured 10 with frequency 1024
Teraz użyjmy dodatkowego kubitu jako generatora losowych bitów — w istocie do rzucania uczciwymi monetami. Użyjemy go, aby losowo wybrać i , a następnie uruchomić protokół superkodowania.
rbg = QuantumRegister(1, "coin")
ebit0 = QuantumRegister(1, "A")
ebit1 = QuantumRegister(1, "B")
Alice_c = ClassicalRegister(1, "Alice c")
Alice_d = ClassicalRegister(1, "Alice d")
test = QuantumCircuit(rbg, ebit0, ebit1, Alice_d, Alice_c)
# Initialize the ebit
test.h(ebit0)
test.cx(ebit0, ebit1)
test.barrier()
# Use the 'coin' qubit twice to generate Alice's bits c and d.
test.h(rbg)
test.measure(rbg, Alice_c)
test.h(rbg)
test.measure(rbg, Alice_d)
test.barrier()
# Now the protocol runs, starting with Alice's actions, which depend
# on her bits.
with test.if_test((Alice_d, 1), label="Z"):
test.z(ebit0)
with test.if_test((Alice_c, 1), label="X"):
test.x(ebit0)
test.barrier()
# Bob's actions
test.cx(ebit0, ebit1)
test.h(ebit0)
test.barrier()
Bob_c = ClassicalRegister(1, "Bob c")
Bob_d = ClassicalRegister(1, "Bob d")
test.add_register(Bob_d)
test.add_register(Bob_c)
test.measure(ebit0, Bob_d)
test.measure(ebit1, Bob_c)
display(test.draw(output="mpl"))
Uruchomienie symulatora Aer pokazuje wyniki: klasyczne bity Alicji i Boba zawsze się zgadzają.
result = AerSimulator().run(test).result()
statistics = result.get_counts()
display(plot_histogram(statistics))
Implementacja w Qiskit
Grę CHSH wraz ze zdefiniowaną powyżej strategią kwantową możemy zaimplementować w Qiskit w następujący sposób.
Najpierw definicja samej gry, która pozwala na podłączenie dowolnej strategii jako argumentu.
def chsh_game(strategy):
# This function runs the CHSH game, using the strategy (a function
# from two bits to two bits), returning 1 for a win and 0 for a loss.
# Choose x and y randomly
x, y = random.randint(0, 1), random.randint(0, 1)
# Use the strategy to determine a and b
a, b = strategy(x, y)
# Decide if the strategy wins or loses
if (a != b) == (x & y):
return 1 # Win
return 0 # Lose
Teraz utworzymy funkcję, która zwraca obwód zależny od pytań zadanych Alicji i Bobowi. Dla uproszczenia pozostawimy kubitom ich domyślne nazwy, a do działań Alicji i Boba wykorzystamy wbudowaną bramkę
def chsh_circuit(x, y):
# This function creates a `QuantumCircuit` implementing the quantum
# strategy described above (including the e-bit preparation).
qc = QuantumCircuit(2, 2)
# Prepare an e-bit
qc.h(0)
qc.cx(0, 1)
qc.barrier()
# Alice's actions
if x == 0:
qc.ry(0, 0)
else:
qc.ry(-pi / 2, 0)
qc.measure(0, 0)
# Bob's actions
if y == 0:
qc.ry(-pi / 4, 1)
else:
qc.ry(pi / 4, 1)
qc.measure(1, 1)
return qc
Oto cztery możliwe obwody w zależności od tego, jakie pytania zostały zadane.
# Draw the four possible circuits
print("(x,y) = (0,0)")
display(chsh_circuit(0, 0).draw(output="mpl"))
print("(x,y) = (0,1)")
display(chsh_circuit(0, 1).draw(output="mpl"))
print("(x,y) = (1,0)")
display(chsh_circuit(1, 0).draw(output="mpl"))
print("(x,y) = (1,1)")
display(chsh_circuit(1, 1).draw(output="mpl"))
(x,y) = (0,0)
(x,y) = (0,1)
(x,y) = (1,0)
(x,y) = (1,1)
Teraz utworzymy zadanie (job) przy użyciu symulatora Aer, które uruchamia obwód jeden raz dla danej pary wejściowej .
def quantum_strategy(x, y):
# This function runs the appropriate quantum circuit defined above
# one time and returns the measurement results
# Setting `shots=1` to run the circuit once
result = AerSimulator().run(chsh_circuit(x, y), shots=1).result()
statistics = result.get_counts()
# Determine the output bits and return them
bits = list(statistics.keys())[0]
a, b = bits[0], bits[1]
return a, b
Na koniec rozegramy grę 1000 razy i obliczymy, jaką część gier strategia wygrywa.
NUM_GAMES = 1000
TOTAL_SCORE = 0
for _ in range(NUM_GAMES):
TOTAL_SCORE += chsh_game(quantum_strategy)
print("Fraction of games won:", TOTAL_SCORE / NUM_GAMES)
Fraction of games won: 0.867
Możemy również zdefiniować strategię klasyczną i zobaczyć, jak dobrze ona działa. Jest to tylko jedna strategia — inne można przetestować, modyfikując kod — ale należy ona do optymalnych strategii klasycznych.
def classical_strategy(x, y):
# This function implements just one example of an optimal classical
# strategy for the CHSH game. Other classical strategies can be
# implemented by changing the bit values assigned to a and b.
# Alice's answer
if x == 0:
a = 0
elif x == 1:
a = 1
# Bob's answer
if y == 0:
b = 1
elif y == 1:
b = 0
return a, b
Ponownie rozegrajmy grę 1000 razy, aby zobaczyć, jak dobrze działa.
NUM_GAMES = 1000
TOTAL_SCORE = 0
for _ in range(NUM_GAMES):
TOTAL_SCORE += chsh_game(classical_strategy)
print("Fraction of games won:", TOTAL_SCORE / NUM_GAMES)
Fraction of games won: 0.747
Choć w grę wchodzi losowość, statystyki po 1000 rozgrywkach z bardzo dużym prawdopodobieństwem nie odbiegną zbytnio od oczekiwanych wyników. Strategia kwantowa wygrywa około 85% czasu, podczas gdy strategia klasyczna nie może wygrywać więcej niż około 75% czasu.