Implementação com Qiskit
Nesta aula, implementamos algumas das ideias da aula sobre entrelaçamento em ação, usando o 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 seguir, uma implementação em circuito quântico do protocolo de teleportação.
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"))
O circuito utiliza alguns recursos do Qiskit que ainda não havíamos visto nas aulas anteriores, incluindo as funções barrier e if_test.
A função barrier cria uma separação visual que torna o diagrama do circuito mais legível, e também impede que o Qiskit realize diversas simplificações e otimizações através da barreira durante a compilação, quando os circuitos são executados em hardware real.
A função if_test aplica uma operação de forma condicional, dependendo de um bit ou registrador clássico.
O circuito primeiro inicializa no estado (o que não faz parte do protocolo em si), seguido pelas operações de Alice, depois suas medições e, por fim, as operações de Bob. Para testar que o protocolo funciona corretamente, aplicaremos uma porta de um único qubit gerada aleatoriamente ao estado inicializado de , obtendo um vetor de estado quântico aleatório a ser teleportado. Ao aplicar a inversa (ou seja, a transposta conjugada) dessa porta em após a execução do protocolo, podemos verificar que o estado foi teleportado medindo para confirmar que ele retornou ao estado .
Primeiro, vamos escolher aleatoriamente uma porta unitária de um 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()))
Agora criaremos um novo circuito de teste que primeiro aplica nossa porta aleatória a , depois executa o circuito de teleportação e, por fim, aplica a inversa da nossa porta aleatória ao qubit e realiza a medição. O resultado deve ser com 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 fim, vamos executar o simulador Aer neste circuito e plotar um histograma das saídas. Veremos as estatísticas para todos os três bits clássicos: o bit inferior/mais à esquerda deve ser sempre , indicando que o qubit foi teleportado com sucesso para , enquanto os outros dois bits devem ser aproximadamente uniformes.
result = AerSimulator().run(test).result()
statistics = result.get_counts()
display(plot_histogram(statistics))
Também podemos filtrar as estatísticas para focar apenas no qubit de resultado do teste, se desejarmos, assim:
filtered_statistics = marginal_distribution(statistics, [2])
display(plot_histogram(filtered_statistics))
Codificação superdensa
A codificação superdensa é um protocolo que, em certo sentido, alcança um objetivo complementar ao da teleportação. Em vez de permitir a transmissão de um qubit usando dois bits clássicos de comunicação (ao custo de um e-bit de entrelaçamento), ela permite a transmissão de dois bits clássicos usando um qubit de comunicação quântica (novamente, ao custo de um e-bit de entrelaçamento).
Em mais detalhes, temos um remetente (Alice) e um receptor (Bob) que compartilham um e-bit de entrelaçamento. De acordo com as convenções estabelecidas para a aula, isso significa que Alice possui um qubit , Bob possui um qubit , e juntos o par está no estado . Alice deseja transmitir dois bits clássicos a Bob, que denotaremos por e , e ela fará isso enviando a ele um qubit.
É razoável considerar essa façanha menos interessante do que a que a teleportação realiza. Enviar qubits provavelmente será muito mais difícil do que enviar bits clássicos no futuro previsível, de modo que trocar um qubit de comunicação quântica por dois bits de comunicação clássica, e ainda por cima ao custo de um e-bit, mal parece valer a pena. No entanto, isso não implica que a codificação superdensa não seja interessante, pois ela certamente é.
Em consonância com o tema da aula, uma das razões pelas quais a codificação superdensa é interessante é que ela demonstra um uso concreto e (no contexto da teoria da informação) bastante notável do entrelaçamento. Um famoso teorema da teoria quântica da informação, conhecido como teorema de Holevo, implica que, sem o uso de um estado entrelaçado compartilhado, é impossível comunicar mais de um bit de informação clássica enviando um único qubit. (O teorema de Holevo é mais geral do que isso. Seu enunciado preciso é técnico e requer explicação, mas essa é uma de suas consequências.) Portanto, por meio da codificação superdensa, o entrelaçamento compartilhado efetivamente permite a duplicação da capacidade de transmissão de informação clássica ao enviar qubits.
Protocolo
O seguinte diagrama de circuito quântico descreve o protocolo de codificação superdensa:

Em palavras, eis o que Alice faz:
-
Se , Alice aplica uma porta ao seu qubit (e se ela não faz nada).
-
Se , Alice aplica uma porta ao seu qubit (e se ela não faz nada).
Alice então envia seu qubit a Bob.
O que Bob faz ao receber o qubit é primeiro aplicar uma porta NOT controlada, com sendo o controle e sendo o alvo, e depois aplica uma porta de Hadamard a . Ele então mede para obter e para obter , usando medições na base padrão em ambos os casos.
Análise
A ideia por trás deste protocolo é simples: Alice efetivamente escolhe qual estado de Bell ela deseja compartilhar com Bob, ela envia a ele seu qubit, e Bob mede para determinar qual estado de Bell Alice escolheu.
Ou seja, eles inicialmente compartilham e dependendo dos bits e Alice deixa esse estado inalterado ou o desloca para um dos outros estados de Bell aplicando ou ao seu qubit
As ações de Bob têm os seguintes efeitos sobre os quatro estados de Bell:
Isso pode ser verificado diretamente, calculando os resultados das operações de Bob sobre esses estados um a um.
Portanto, quando Bob realiza suas medições, ele consegue determinar qual estado de Bell Alice escolheu. Verificar que o protocolo funciona corretamente é uma questão de checar cada caso:
-
Se , então o estado de quando Bob recebe é . Ele transforma esse estado em e obtém .
-
Se , então o estado de quando Bob recebe é . Ele transforma esse estado em e obtém .
-
Se , então o estado de quando Bob recebe é . Ele transforma esse estado em e obtém .
-
Se , então o estado de quando Bob recebe é . Ele transforma esse estado em e obtém . (O fator de fase negativo não tem efeito aqui.)
Implementação da codificação superdensa
A seguir, uma implementação simples da codificação superdensa, em que especificamos o circuito em si de acordo com os bits a serem transmitidos. Primeiro, escolheremos dois bits a serem transmitidos. (Mais adiante os escolheremos aleatoriamente, mas por enquanto faremos apenas uma escolha arbitrária.)
c = "1"
d = "0"
Agora construiremos o circuito de acordo com isso. Aqui permitiremos que o Qiskit use os nomes padrão para os qubits: para o qubit superior e para o 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"))
Não há muitas novidades aqui, exceto a função measure_all, que mede todos os qubits e coloca os resultados em um único registrador clássico (neste caso, com dois bits).
Executar o simulador Aer produz a saída 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
Agora vamos usar um qubit adicional como gerador de bits aleatórios — essencialmente para lançar moedas honestas. Vamos usá-lo para escolher aleatoriamente e , e então executar o protocolo de codificação 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"))
Executar o simulador Aer mostra os resultados: os bits clássicos de Alice e Bob sempre concordam.
result = AerSimulator().run(test).result()
statistics = result.get_counts()
display(plot_histogram(statistics))
Implementação em Qiskit
Podemos implementar o jogo CHSH, junto com a estratégia quântica definida acima, em Qiskit da seguinte forma.
Primeiro, veja a definição do próprio jogo, que aceita uma estratégia arbitrária 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
Agora vamos criar uma função que retorna um circuito dependendo das perguntas feitas a Alice e Bob. Vamos deixar os qubits com seus nomes padrão para simplificar, e usaremos a porta embutida para as ações de Alice e 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
Aqui estão os quatro circuitos possíveis, dependendo de quais perguntas são feitas.
# 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)
Agora vamos criar um job usando o simulador Aer que executa o circuito uma única vez para um par de entradas fornecido.
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 fim, vamos jogar o jogo 1000 vezes e calcular a fração de vitórias da estratégia.
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
Também podemos definir uma estratégia clássica e ver como ela se sai. Esta é apenas uma estratégia — outras podem ser testadas alterando o código — mas ela está entre as estratégias clássicas ótimas.
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
Vamos jogar o jogo mais 1000 vezes para ver como ela se sai.
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
Embora haja aleatoriedade envolvida, é muito improvável que as estatísticas se desviem demais após 1000 execuções. A estratégia quântica vence cerca de 85% das vezes, enquanto uma estratégia clássica não consegue vencer mais do que cerca de 75% das vezes.