Implementación con Qiskit
En esta lección implementamos algunas de las ideas de la lección sobre entrelazamiento en acción, usando Qiskit.
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"qiskit": "qiskit", "qiskit_aer": "qiskit-aer"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
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
A continuación se muestra una implementación del protocolo de teletransportación mediante un circuito cuántico.
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"))
El circuito utiliza algunas características de Qiskit que aún no habíamos visto en lecciones anteriores, como las funciones barrier e if_test.
La función barrier crea una separación visual que hace el diagrama del circuito más legible, y además impide que Qiskit aplique diversas simplificaciones y optimizaciones a través de la barrera durante la compilación cuando los circuitos se ejecutan en hardware real.
La función if_test aplica una operación de forma condicional dependiendo de un bit o registro clásico.
El circuito primero inicializa en el estado (lo cual no forma parte del protocolo en sí), seguido de las operaciones de Alice, luego sus mediciones y, finalmente, las operaciones de Bob. Para verificar que el protocolo funciona correctamente, aplicaremos una compuerta de un solo qubit generada aleatoriamente al estado inicializado de , con el fin de obtener un vector de estado cuántico aleatorio para teleportar. Al aplicar la inversa (es decir, la transpuesta conjugada) de esa compuerta a después de ejecutar el protocolo, podemos verificar que el estado fue teleportado midiendo para comprobar que ha vuelto al estado .
Primero elegiremos aleatoriamente una compuerta unitaria de un qubit.
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()))
Ahora crearemos un nuevo circuito de prueba que primero aplica nuestra compuerta aleatoria a , luego ejecuta el circuito de teletransportación y, finalmente, aplica la inversa de nuestra compuerta aleatoria al qubit y mide. El resultado debería ser con certeza.
# 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"))
Por último, ejecutemos el simulador Aer en este circuito y grafiquemos un histograma de las salidas. Veremos las estadísticas de los tres bits clásicos: el bit inferior/más a la izquierda siempre debería ser , lo que indica que el qubit fue teleportado exitosamente a , mientras que los otros dos bits deberían ser aproximadamente uniformes.
result = AerSimulator().run(test).result()
statistics = result.get_counts()
display(plot_histogram(statistics))
También podemos filtrar las estadísticas para centrarnos únicamente en el qubit de resultado de la prueba si lo deseamos, de esta manera:
filtered_statistics = marginal_distribution(statistics, [2])
display(plot_histogram(filtered_statistics))
Codificación superdensa
La codificación superdensa es un protocolo que, en cierto sentido, persigue un objetivo complementario al de la teletransportación. En lugar de permitir la transmisión de un qubit usando dos bits clásicos de comunicación (al costo de un e-bit de entrelazamiento), permite la transmisión de dos bits clásicos usando un qubit de comunicación cuántica (también al costo de un e-bit de entrelazamiento).
En detalle, tenemos un emisor (Alicia) y un receptor (Bob) que comparten un e-bit de entrelazamiento. Según las convenciones de la lección, esto significa que Alicia tiene un qubit , Bob tiene un qubit , y juntos el par se encuentra en el estado . Alicia desea transmitirle a Bob dos bits clásicos, que denotaremos como y , y lo logrará enviándole un qubit.
Es razonable considerar esta hazaña menos interesante que la que logra la teletransportación. Es probable que enviar qubits siga siendo mucho más difícil que enviar bits clásicos en el futuro previsible, así que intercambiar un qubit de comunicación cuántica por dos bits de comunicación clásica, con el costo de un e-bit encima, no parece valer la pena. Sin embargo, esto no implica que la codificación superdensa no sea interesante, porque sin duda lo es.
Siguiendo el tema de la lección, una razón por la que la codificación superdensa es interesante es que demuestra un uso concreto y (en el contexto de la teoría de la información) bastante llamativo del entrelazamiento. Un famoso teorema de la teoría de la información cuántica, conocido como el teorema de Holevo, implica que sin el uso de un estado entrelazado compartido, es imposible comunicar más de un bit de información clásica enviando un único qubit. (El teorema de Holevo es más general que esto. Su enunciado preciso es técnico y requiere explicación, pero esta es una de sus consecuencias.) Así, mediante la codificación superdensa, el entrelazamiento compartido permite efectivamente duplicar la capacidad de transmisión de información clásica al enviar qubits.
Protocolo
El siguiente diagrama de circuito cuántico describe el protocolo de codificación superdensa:

En palabras, esto es lo que hace Alicia:
-
Si , Alicia aplica una compuerta a su qubit (y si no hace nada).
-
Si , Alicia aplica una compuerta a su qubit (y si no hace nada).
Luego, Alicia le envía su qubit a Bob.
Cuando Bob recibe el qubit , primero aplica una compuerta NOT controlada, con como control y como objetivo, y luego aplica una compuerta de Hadamard a . Después mide para obtener y para obtener , usando mediciones en la base estándar en ambos casos.
Análisis
La idea detrás de este protocolo es sencilla: Alicia elige efectivamente qué estado de Bell quiere compartir con Bob, le envía su qubit, y Bob mide para determinar cuál estado de Bell eligió Alicia.
Es decir, inicialmente comparten y dependiendo de los bits y Alicia deja ese estado intacto o lo transforma en uno de los otros estados de Bell aplicando o a su qubit
Las acciones de Bob producen los siguientes efectos sobre los cuatro estados de Bell:
Esto puede verificarse directamente calculando los resultados de las operaciones de Bob sobre cada uno de estos estados por separado.
Así, cuando Bob realiza sus mediciones, puede determinar cuál estado de Bell eligió Alicia. Verificar que el protocolo funciona correctamente consiste en revisar cada caso:
-
Si , entonces el estado de cuando Bob recibe es . Él transforma este estado en y obtiene .
-
Si , entonces el estado de cuando Bob recibe es . Él transforma este estado en y obtiene .
-
Si , entonces el estado de cuando Bob recibe es . Él transforma este estado en y obtiene .
-
Si , entonces el estado de cuando Bob recibe es . Él transforma este estado en y obtiene . (El factor de fase negativo no tiene ningún efecto aquí.)
Implementación de codificación superdensa
A continuación se presenta una implementación simple de la codificación superdensa donde se especifica el circuito en función de los bits a transmitir. Primero elegiremos dos bits para transmitir. (Más adelante los elegiremos de forma aleatoria, pero por ahora simplemente haremos una elección arbitraria.)
c = "1"
d = "0"
Ahora construiremos el circuito correspondiente. Aquí le dejaremos a Qiskit usar los nombres por defecto para los qubits: para el qubit superior y para el inferior.
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"))
Aquí no hay mucho de nuevo, excepto la función measure_all, que mide todos los qubits y coloca los resultados en un único registro clásico (que en este caso contiene dos bits).
Al ejecutar el simulador Aer se obtiene la salida esperada.
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
Ahora usaremos un qubit adicional como generador de bits aleatorios — esencialmente para lanzar monedas equilibradas. Lo utilizaremos para elegir y de forma aleatoria, y luego ejecutaremos el protocolo de codificación superdensa.
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"))
Al ejecutar el simulador Aer se muestran los resultados: los bits clásicos de Alicia y Bob siempre coinciden.
result = AerSimulator().run(test).result()
statistics = result.get_counts()
display(plot_histogram(statistics))
Implementación en Qiskit
Podemos implementar el juego CHSH, junto con la estrategia cuántica definida anteriormente, en Qiskit de la siguiente manera.
Primero, aquí está la definición del juego en sí, que permite conectar una estrategia arbitraria como argumento.
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
Ahora crearemos una función que genera un circuito dependiendo de las preguntas para Alicia y Bob. Dejaremos que los qubits tengan sus nombres por defecto para simplificar, y usaremos la compuerta incorporada para las acciones de Alicia y Bob.
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
Aquí están los cuatro circuitos posibles, dependiendo de qué preguntas se hagan.
# 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)
Ahora crearemos un trabajo usando el simulador Aer que ejecuta el circuito una sola vez para un par de entrada dado .
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
Por último, jugaremos el juego 1000 veces y calcularemos la fracción de veces que la estrategia gana.
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
También podemos definir una estrategia clásica y ver qué tan bien funciona. Esta es solo una estrategia — se pueden probar otras cambiando el código — pero está entre las estrategias clásicas óptimas.
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
Juguemos de nuevo el juego 1000 veces para ver qué tan bien funciona.
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
Aunque hay aleatoriedad involucrada, es muy poco probable que las estadísticas se desvíen demasiado después de 1000 ejecuciones. La estrategia cuántica gana aproximadamente el 85% de las veces, mientras que una estrategia clásica no puede ganar más de aproximadamente el 75% de las veces.