Przejdź do głównej treści

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"))

Wynik poprzedniej komórki z kodem

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 (A,B)(\mathsf{A},\mathsf{B}) w stanie ∣ϕ+⟩\vert \phi^+\rangle (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 ∣0⟩\vert 0\rangle kubitu Q\mathsf{Q}, aby otrzymać losowy wektor stanu kwantowego, który zostanie teleportowany. Stosując odwrotność (czyli transpozycję sprzężoną) tej bramki do B\mathsf{B} po wykonaniu protokołu, możemy zweryfikować, że stan został teleportowany, mierząc, czy powrócił do stanu ∣0⟩\vert 0\rangle.

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()))
[0.9897212158−0.0195080103−0.141673401i0.0603319186+0.1296609988i−0.8319925233+0.5360378028i] \begin{bmatrix} 0.9897212158 & -0.0195080103 - 0.141673401 i \\ 0.0603319186 + 0.1296609988 i & -0.8319925233 + 0.5360378028 i \\ \end{bmatrix}

Teraz utworzymy nowy obwód testowy, który najpierw stosuje naszą losową bramkę do Q\mathsf{Q}, następnie uruchamia obwód teleportacji, a na koniec stosuje odwrotność naszej losowej bramki do kubitu B\mathsf{B} i wykonuje pomiar. Wynik powinien być 00 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"))

Wynik poprzedniej komórki z kodem

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ć 00, co wskazuje, że kubit Q\mathsf{Q} został pomyślnie teleportowany do B\mathsf{B}, 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))

Wynik poprzedniej komórki z kodem

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))

Wynik poprzedniej komórki z kodem

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 A\mathsf{A}, Bob ma kubit B\mathsf{B}, a wspólnie para (A,B)(\mathsf{A},\mathsf{B}) jest w stanie ∣ϕ+⟩\vert\phi^+\rangle. Alicja chce przesłać Bobowi dwa klasyczne bity, które oznaczymy jako cc i dd, 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:

Obwód superkodowania

Słownie, oto co robi Alicja:

  1. Jeśli d=1d=1, Alicja wykonuje bramkę ZZ na swoim kubicie A\mathsf{A} (a jeśli d=0d=0, nie wykonuje).

  2. Jeśli c=1c=1, Alicja wykonuje bramkę XX na swoim kubicie A\mathsf{A} (a jeśli c=0c=0, nie wykonuje).

Następnie Alicja wysyła swój kubit A\mathsf{A} do Boba.

Kiedy Bob otrzymuje kubit A\mathsf{A}, najpierw wykonuje bramkę controlled-NOT, gdzie A\mathsf{A} jest bramką sterującą, a B\mathsf{B} jest bramką celu, a następnie stosuje bramkę Hadamard na A\mathsf{A}. Potem mierzy B\mathsf{B}, aby uzyskać cc, oraz A\mathsf{A}, aby uzyskać dd, 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 ∣ϕ+⟩,\vert\phi^+\rangle, a w zależności od bitów cc i d,d, Alicja albo pozostawia ten stan bez zmian, albo przekształca go w jeden z pozostałych stanów Bella, stosując I,\mathbb{I}, X,X, ZZ lub XZXZ na swoim kubicie A.\mathsf{A}.

(I⊗I)∣ϕ+⟩=∣ϕ+⟩(I⊗Z)∣ϕ+⟩=∣ϕ−⟩(I⊗X)∣ϕ+⟩=∣ψ+⟩(I⊗XZ)∣ϕ+⟩=∣ψ−⟩\begin{aligned} (\mathbb{I} \otimes \mathbb{I}) \vert \phi^+ \rangle & = \vert \phi^+\rangle \\ (\mathbb{I} \otimes Z) \vert \phi^+ \rangle & = \vert \phi^-\rangle \\ (\mathbb{I} \otimes X) \vert \phi^+ \rangle & = \vert \psi^+\rangle \\ (\mathbb{I} \otimes XZ) \vert \phi^+ \rangle & = \vert \psi^-\rangle \end{aligned}

Działania Boba mają następujące efekty na czterech stanach Bella:

∣ϕ+⟩↦∣00⟩∣ϕ−⟩↦∣01⟩∣ψ+⟩↦∣10⟩∣ψ−⟩↦−∣11⟩\begin{aligned} \vert \phi^+\rangle & \mapsto \vert 00\rangle\\ \vert \phi^-\rangle & \mapsto \vert 01\rangle\\ \vert \psi^+\rangle & \mapsto \vert 10\rangle\\ \vert \psi^-\rangle & \mapsto -\vert 11\rangle\\ \end{aligned}

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 cd=00cd = 00, to stan (B,A)(\mathsf{B},\mathsf{A}) w chwili, gdy Bob otrzymuje A\mathsf{A}, to ∣ϕ+⟩\vert \phi^+\rangle. Przekształca ten stan w ∣00⟩\vert 00\rangle i uzyskuje cd=00cd = 00.

  • Jeśli cd=01cd = 01, to stan (B,A)(\mathsf{B},\mathsf{A}) w chwili, gdy Bob otrzymuje A\mathsf{A}, to ∣ϕ−⟩\vert \phi^-\rangle. Przekształca ten stan w ∣01⟩\vert 01\rangle i uzyskuje cd=01cd = 01.

  • Jeśli cd=10cd = 10, to stan (B,A)(\mathsf{B},\mathsf{A}) w chwili, gdy Bob otrzymuje A\mathsf{A}, to ∣ψ+⟩\vert \psi^+\rangle. Przekształca ten stan w ∣10⟩\vert 10\rangle i uzyskuje cd=10cd = 10.

  • Jeśli cd=11cd = 11, to stan (B,A)(\mathsf{B},\mathsf{A}) w chwili, gdy Bob otrzymuje A\mathsf{A}, to ∣ψ−⟩\vert \psi^-\rangle. Przekształca ten stan w −∣11⟩-\vert 11\rangle i uzyskuje cd=11cd = 11. (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: q0\mathsf{q}_0 dla górnego kubitu i q1\mathsf{q}_1 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"))

Wynik poprzedniej komórki kodu

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

Wynik poprzedniej komórki kodu

Teraz użyjmy dodatkowego kubitu jako generatora losowych bitów — w istocie do rzucania uczciwymi monetami. Użyjemy go, aby losowo wybrać cc i dd, 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"))

Wynik poprzedniej komórki kodu

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))

Wynik poprzedniej komórki kodu

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ę Ry(θ).R_y(\theta).

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)

Wynik poprzedniej komórki kodu

(x,y) = (0,1)

Wynik poprzedniej komórki kodu

(x,y) = (1,0)

Wynik poprzedniej komórki kodu

(x,y) = (1,1)

Wynik poprzedniej komórki kodu

Teraz utworzymy zadanie (job) przy użyciu symulatora Aer, które uruchamia obwód jeden raz dla danej pary wejściowej (x,y)(x,y).

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.