Show code cell source
!pip install -U -q pip
!pip install -U -q numpy ket-lang plotly
import plotly.graph_objs as go
import numpy as np
import ket
def bloch_sphere(ket: np.array) -> go.Figure:
"""Retorna a figura de uma esfera de Bloch com estado do ket marcado na esfera.
Args:
ket (np.array): Estado quântico para marcar na esfera.
Returns:
go.Figure: Figura com a esfera de Bloch para plot.
"""
phi = np.linspace(0, np.pi, 20)
theta = np.linspace(0, 2 * np.pi, 40)
phi, theta = np.meshgrid(phi, theta)
x = np.sin(phi) * np.cos(theta)
y = np.sin(phi) * np.sin(theta)
z = np.cos(phi)
sphere = go.Surface(
x=x, y=y, z=z, showscale=False, opacity=0.02, name="Bloch Sphere"
)
equator_theta = np.linspace(0, 2 * np.pi, 100)
equator_x = np.cos(equator_theta)
equator_y = np.sin(equator_theta)
equator_z = np.zeros_like(equator_theta)
equator = go.Scatter3d(
x=equator_x,
y=equator_y,
z=equator_z,
mode="lines",
line=dict(color="gray", width=3),
opacity=0.1,
name="Equator",
)
z_line = go.Scatter3d(
x=[0, 0],
y=[0, 0],
z=[1, -1],
mode="lines",
line=dict(color="gray", width=3),
opacity=0.1,
name="z line",
)
x_line = go.Scatter3d(
x=[1, -1],
y=[0, 0],
z=[0, 0],
mode="lines",
line=dict(color="gray", width=3),
opacity=0.1,
name="x line",
)
y_line = go.Scatter3d(
x=[0, 0],
y=[1, -1],
z=[0, 0],
mode="lines",
line=dict(color="gray", width=3),
opacity=0.1,
name="y line",
)
basis_points = [
([0, 0, 1], "|0⟩"),
([0, 0, -1], "|1⟩"),
([1, 0, 0], "|+⟩"),
([-1, 0, 0], "|‒⟩"),
([0, 1, 0], "|i+⟩"),
([0, -1, 0], "|i‒⟩"),
]
basis = [
go.Scatter3d(
x=[p[0]],
y=[p[1]],
z=[p[2]],
mode="text",
text=[text],
textposition="middle center",
name=text,
)
for p, text in basis_points
]
bra = np.conjugate(ket.T)
X = np.array([[0, 1], [1, 0]])
Y = np.array([[0, -1j], [1j, 0]])
Z = np.array([[1, 0], [0, -1]])
exp_x = (bra @ X @ ket).item().real
exp_y = (bra @ Y @ ket).item().real
exp_z = (bra @ Z @ ket).item().real
qubit = go.Scatter3d(
x=[exp_x],
y=[exp_y],
z=[exp_z],
mode="markers",
marker=dict(size=5, color="red"),
name="qubit",
)
line = go.Scatter3d(
x=[0, exp_x],
y=[0, exp_y],
z=[0, exp_z],
mode="lines",
line=dict(color="red", width=3),
opacity=0.5,
name="qubit line",
)
fig = go.Figure(
data=[
sphere,
qubit,
line,
equator,
x_line,
y_line,
z_line,
*basis,
]
)
fig.update_layout(
scene=dict(
xaxis=dict(
range=[-1, 1],
showgrid=False,
showbackground=False,
visible=False,
),
yaxis=dict(
range=[-1, 1],
showgrid=False,
showbackground=False,
visible=False,
),
zaxis=dict(
range=[-1, 1],
showgrid=False,
showbackground=False,
visible=False,
),
aspectmode="cube",
),
showlegend=False,
)
return fig
Postulados da Mecânica Quântica#
Neste capítulo, apresentaremos os quatro postulados da mecânica quântica enumerados por Nielsen e Chuang [NC10], com ênfase em sua aplicação na computação quântica. Esses postulados constituem um formalismo matemático que descreve o comportamento de sistemas quânticos fechados. Além disso, abordaremos a implementação de cada postulado utilizando Python com NumPy, construindo um simulador de computação quântica no processo.
Postulado 1 - Espaço do Sistema#
O primeiro postulado trata da representação do sistema quântico.
Associado a qualquer sistema físico isolado, existe um espaço de vetores complexos com produto interno (ou seja, um espaço de Hilbert) conhecido como o espaço de estados do sistema. O sistema é completamente descrito pelo seu vetor de estado, sendo um vetor unitário no espaço de estados do sistema.
Na computação quântica, temos como unidade básica de computação o bit quântico ou qubit. Analogamente ao bit clássico, este é um sistema de dois níveis, ou seja, possui dois valores possíveis. Na computação clássica, os possíveis estados de um bit são usualmente representados como 0
e 1
. Já na computação quântica, representamos os possíveis estados de um qubit com \(\ket{0}\) e \(\ket{1}\), utilizando a notação de Dirac vista no capítulo de álgebra linear. Em conformidade com o primeiro postulado, podemos representar um qubit como sendo um vetor unitário pertencente ao espaço \(\mathbb{C}^2\), onde os elementos da base canônica formam os estados \(\ket{0}\) e \(\ket{1}\). Essa base também é conhecida como base computacional na computação quântica.
Base computacional:
Mantendo a restrição de que um qubit precisa ser um vetor unitário, podemos escrever o estado de um qubit qualquer como sendo uma combinação dos vetores da base computacional:
Onde \(|\alpha|^2+|\beta|^2=1\).
Dessa forma, se os valores \(\alpha\) e \(\beta\), conhecidos como amplitudes de probabilidade, forem diferentes de zero, esse qubit está em um estado de superposição, estando tanto no estado \(\ket{0}\) quanto no estado \(\ket{1}\) ao mesmo tempo.
Além disso, utilizando NumPy, podemos definir os estados da base computacional e o estado de um qubit qualquer da seguinte maneira:
# Definindo os estados da base computacional
ket0 = np.array([[1, 0]]).T
ket1 = np.array([[0, 1]]).T
print("|0⟩ =\n", ket0)
print("|1⟩ =\n", ket1)
# Definindo o estado de um qubit qualquer
qubit = 1 / np.sqrt(2) * ket0 + 1 / np.sqrt(2) * ket1
# Verificando se o estado do qubit é unitário
norm_qubit = np.linalg.norm(qubit)
assert np.isclose(norm_qubit, 1), "Erro: O estado do qubit não é unitário"
print("Estado de um qubit qualquer:\n", qubit)
|0⟩ =
[[1]
[0]]
|1⟩ =
[[0]
[1]]
Estado de um qubit qualquer:
[[0.70710678]
[0.70710678]]
Postulado 2 - Evolução#
O segundo postulado trata da evolução do sistema quântico, ou seja, do processo de computação.
A evolução de um sistema quântico fechado é descrita por uma transformação unitária. Em outras palavras, o estado \(\left| \psi_1 \right>\) do sistema no tempo \(t_1\) está relacionado ao estado \(\left| \psi_2 \right>\) do sistema no tempo \(t_2\) por um operador unitário \(U\) que depende apenas dos tempos \(t_1\) e \(t_2\):
\[ U\left| \psi_1 \right> = \left| \psi_2 \right> \]
Este postulado descreve que os passos de computação são descritos por operadores unitários, que na computação quântica são conhecidos como portas lógicas quânticas. Abaixo estão listadas algumas portas quânticas de um qubit:
Veremos de múltiplos qubits após o Postulado 4.
A aplicação das portas lógicas quânticas é feita através de multiplicação matriz-vetor, como mostrado no código abaixo:
# Definindo as matrizes das portas lógicas quânticas
X = np.array([[0, 1], [1, 0]])
Y = np.array([[0, -1j], [1j, 0]])
Z = np.array([[1, 0], [0, -1]])
H = np.array([[1, 1], [1, -1]]) / np.sqrt(2)
I = np.eye(2)
# Verificar que uma matrixes são unitária
for gate in [X, Y, Z, H]:
assert np.allclose(gate @ np.conj(gate.T), I), "Erro: a matrix não é unitária"
# Aplicando as portas lógicas quânticas aos estados da base computacional
ket_mais = H @ ket0
ket_menos = H @ ket1
print("|+⟩ =\n", ket_mais)
print("|‒⟩ =\n", ket_menos)
|+⟩ =
[[0.70710678]
[0.70710678]]
|‒⟩ =
[[ 0.70710678]
[-0.70710678]]
Esfera de Bloch#
A Esfera de Bloch é uma representação geométrica tridimensional de um bit quântico, que oferece uma maneira intuitiva de visualizar e entender seu estado. Nesta esfera, cada ponto na superfície representa um estado possível do qubit. Por exemplo, os estados básicos \(\ket{0}\) e \(\ket{1}\) são representados pelos polos norte e sul da esfera, respectivamente. Além disso, estados superpostos são representados por pontos em outras posições da superfície da esfera.
Uma característica importante da Esfera de Bloch é que os estados ortogonais são representados por pontos diametralmente opostos na esfera. Além de fornecer uma representação visual dos estados de um qubit, a Esfera de Bloch também é útil para entender operações quânticas. Por exemplo, rotações ao redor dos eixos da esfera representam operações de porta quântica, como a aplicação de portas de Pauli e a operação de Hadamard.
A função bloch_sphere
, definida neste notebook, plota uma Esfera de Bloch a partir de um estado quântico. É possível utilizá-la para compreender como uma porta lógica altera o estado de um qubit. Por exemplo, o código abaixo plota o qubit no estado \(\ket{0}\) e, em seguida, mostra a aplicação de uma porta Hadamard:
print("|0⟩ =\n", ket0)
bloch_sphere(ket0)
|0⟩ =
[[1]
[0]]
psi = H @ ket0
print("H|0⟩ =\n", psi)
bloch_sphere(psi)
H|0⟩ =
[[0.70710678]
[0.70710678]]
Exercício: Portas de Rotação Parametrizadas#
Complete o código abaixo implementando as portas quânticas parametrizadas de rotação e, em seguida, verifique quais parâmetros ou sequências de portas de rotação implementam o mesmo comportamento das portas de Pauli e da porta de Hadamard em um qubit. Você pode usar a função bloch_sphere
para verificar o estado quântico após a computação. Note que, para este exercício, a fase global é desconsiderada. Dica: se o estado final da aplicação da porta no estado \(\ket{0}\) e \(\ket{1}\) são iguais, as portas são equivalentes.
Mais detalhes
Este exercício tem como objetivo explorar como as portas de rotação parametrizadas podem ser utilizadas para simular o comportamento das portas de Pauli (X, Y, Z) e da porta de Hadamard em um qubit. Você deverá completar a implementação das funções RX
, RY
e RZ
para criar as portas de rotação em torno dos eixos X, Y e Z, respectivamente. Em seguida, você deverá encontrar quais parâmetros ou sequências de portas de rotação produzem o mesmo resultado que as portas de Pauli e a porta de Hadamard quando aplicadas a um qubit no estado \(\ket{0}\) ou \(\ket{1}\). Use a função bloch_sphere
para visualizar o estado quântico após a aplicação das portas e verificar se os resultados são equivalentes.
def RX(theta: float) -> np.array:
"""Retorna um operador unitário que aplica uma rotação theta em torno do eixo X.
Args:
theta (float): Parâmetro da rotação.
Returns:
np.array: Operador unitário.
"""
# Implemente a porta de rotação em torno do eixo X
...
def RY(theta: float) -> np.array:
"""Retorna um operador unitário que aplica uma rotação theta em torno do eixo Y.
Args:
theta (float): Parâmetro da rotação.
Returns:
np.array: Operador unitário.
"""
# Implemente a porta de rotação em torno do eixo Y
...
def RZ(theta: float) -> np.array:
"""Retorna um operador unitário que aplica uma rotação theta em torno do eixo Z.
Args:
theta (float): Parâmetro da rotação.
Returns:
np.array: Operador unitário.
"""
# Implemente a porta de rotação em torno do eixo Z
...
Postulado 3 - Medida#
O terceiro postulado diz respeito à medida do estado quântico.
Medições quânticas são descritas por uma coleção \(\{M_m\}\) de operadores de medição. Esses operadores atuam no espaço de estados do sistema que está sendo medido. O índice \(m\) se refere aos resultados de medição que podem ocorrer no experimento. Se o estado do sistema quântico é \(\left| \psi \right>\) imediatamente antes da medição, então a probabilidade de que o resultado \(m\) ocorra é dada por
\[ p(m) = \left< \psi \right|M_m^\dagger M_m\left| \psi \right>, \]e o estado do sistema após a medição é
\[ \frac{M_m\left| \psi \right>}{\sqrt{\left< \psi \right|M_m^\dagger M_m\left| \psi \right>}}. \]Os operadores de medição satisfazem a equação de completude,
\[ \sum_m M_m^\dagger M_m = I. \]
O postulado é apresentado de forma genérica, no entanto, na computação quântica, normalmente utilizamos medidas na base computacional com a seguinte coleção de operadores de medição:
Abaixo, é implementada uma função de medida baseada no postulado 3. Utilize-as para resolver os próximos exercícios.
def probabilidade(ket: np.array, operador: np.array) -> float:
"""Retorna a probabilidade de medir o estado correspondente ao operador
Args:
ket (np.array): Estado quântico.
operador (np.array): Operador de medição.
Returns:
float: Probabilidade de medir o estado correspondente ao operador.
"""
conj_T = lambda matrix: np.conj(matrix.T)
bra = conj_T(ket)
return (bra @ conj_T(operador) @ operador @ ket).item().real
def colapsar_estado(ket: np.array, operador: np.array) -> np.array:
"""Colapsa o estado quântico.
Args:
ket (np.array): Estado Quântico
operador (np.array): Operador de medição;
Returns:
np.array: Estado quântico colapsado.
"""
return operador @ ket / np.sqrt(probabilidade(ket, operador))
def medir(ket: np.array, operadores: list[np.array]) -> tuple[np.array, int]:
"""Mede e colapsa o estado quântico;
Args:
ket (np.array): Estado Quântico.
operadores (list[np.array]): Conjunto de operadores de medição.
Returns:
tuple[np.array, int]: Estado colapsado e índice medido.
"""
assert np.allclose(np.sum(operadores, axis=0), np.eye(operadores[0].shape[0]))
probabilidades = [probabilidade(ket, operador) for operador in operadores]
index = np.random.choice(range(len(operadores)), p=probabilidades)
return colapsar_estado(ket, operadores[index]), index
Exercício: Geração de Estados Quânticos com Distribuições de Probabilidades Específicas#
Neste exercício, seu objetivo é gerar estados quânticos com distribuições de probabilidades específicas. Você deve implementar diferentes distribuições de probabilidades e verificar se os estados quânticos gerados correspondem às probabilidades desejadas.
Gere um estado quântico em que as probabilidades de medir 0 e 1 sejam iguais.
Gere um estado quântico em que haja uma probabilidade de 75% de medir 0 e uma probabilidade de 25% de medir 1.
Gere um estado quântico em que haja uma probabilidade de 1/3 de medir 0 e uma probabilidade de 2/3 de medir 1.
Implemente uma distribuição de probabilidade parametrizada, onde você possa especificar as probabilidades desejadas como parâmetros de entrada.
Dica: Utilize as funções definidas para medir a probabilidade e também as portas de rotação implementadas no exercício anterior. As medidas são consideradas na base computacional.
print("1. Iguais probabilidade de medir 0 e 1")
print("...")
1. Iguais probabilidade de medir 0 e 1
...
print("2. 75% de probabilidade de medir 0 e 25% de medir 1")
print("...")
2. 75% de probabilidade de medir 0 e 25% de medir 1
...
print("3. 1/3 de probabilidade de medir 0 e 2/3 de medir 1")
print("...")
3. 1/3 de probabilidade de medir 0 e 2/3 de medir 1
...
p0 = 0.22
p1 = 1.0 - p0
print(f"4. {p0*100:0.2f}% de probabilidade de medir 0 e {p1*100:0.2f}% de medir 1")
print("...")
4. 22.00% de probabilidade de medir 0 e 78.00% de medir 1
...
Postulado 4 - Sistema Composto#
O quarto postulado nos fornece a ferramenta necessária para aplicar os outros postulados em sistemas com múltiplos qubits.
O espaço de um sistema físico composto é o produto tensorial dos subespaços. Além disso, se temos sistemas numerados de \(1\) a \(n\), e o sistema número \(i\) é preparado no estado \(\left| \psi_i \right>\), então o estado conjunto do sistema total é \(\left| \psi_1 \right>\otimes\left| \psi_2 \right>\otimes\cdots\otimes\left| \psi_n \right>\).
Dica
Para saber mais sobre produto tensorial, consulte o capítulo de álgebra linear.
Segundo o postulado, podemos representar um sistema de \(n\) qubits (não entrelaçados) como:
Assim, por exemplo, podemos criar uma base para um sistema de 2 qubits a partir da base de 1 qubit, como:
Exemplo: Preparação do Estado de Bell#
O estado de Bell é um estado entrelaçado de dois qubits, onde cada qubit possui igual probabilidade de medir 0 ou 1. No entanto, devido à relação de entrelaçamento, a medida de um qubit é sempre igual à medida do outro.
Um argumento para demonstrar que o estado de Bell está entrelaçado é a impossibilidade de descrever cada qubit individualmente, não sendo possível satisfazer a seguinte equação:
É possível preparar um estado de Bell a partir do estado base \(\ket{00}\), mas para isso precisamos apresentar uma porta de dois qubits, a \(\text{CNOT}\):
Assim, podemos executar os seguintes passos para preparar o estado:
Usando NumPy, podemos descrever essa evolução como:
# Definição da matriz CNOT
CNOT = np.array(
[
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 0, 1],
[0, 0, 1, 0],
]
)
# Criação da matriz HI utilizando o produto tensorial (kron)
HI = np.kron(H, I)
# Criação do estado |00⟩ utilizando o produto tensorial (kron)
ket00 = np.kron(ket0, ket0)
# Aplicação da sequência de operações para preparar o estado de Bell
bell = CNOT @ HI @ ket00
print("|bell⟩ =\n", bell)
|bell⟩ =
[[0.70710678]
[0. ]
[0. ]
[0.70710678]]
Exercício: Implementação do Protocolo de Teletransporte Quântico#
O protocolo de teletransporte quântico é uma técnica que permite a transferência de informações quânticas de um qubit para outro, mesmo à distância, utilizando canais clássicos e um par de qubits entrelaçados.
O circuito quântico abaixo ilustra o protocolo de teletransporte:
Neste exercício, seu objetivo é entender e implementar o protocolo de teletransporte quântico utilizando as operações definidas neste capítulo. Você utilizará as portas lógicas quânticas e as operações de medição para realizar o teletransporte de um estado quântico de um qubit para outro.
Dica
\(\ket{\Phi}^+ = \frac{1}{\sqrt{2}}(\ket{00}+\ket{11})\).
from functools import reduce
def teleporte(qubits: np.array) -> tuple[np.array, int, int]:
"""
Implementa o protocolo de teletransporte quântico para transferir um estado
quântico de um qubit para outro.
Args:
qubits (np.array): Um array numpy que representa os qubits envolvidos no
protocolo de teletransporte. O primeiro qubit representa o estado
quântico da mensagem, e o segundo o estado quântico entrelaçado
entre Alice e Bob.
Returns:
tuple[np.array, int, int]: Uma tupla contendo o estado final dos qubits
após o teletransporte, juntamente com os resultados das medidas
realizadas na Mensagem e Alice.
"""
...
def verificar_teleporte():
T = np.array([[0, 1], [0, np.exp(1j * np.pi / 4)]])
# Prepara um qubit para a mensagem
mensagem = ket0
mensagem = T @ H @ mensagem
# Prepara o estado entrelaçado entre Alice e Bob
alice_bob = CNOT @ np.kron(H @ ket0, ket0)
# Realiza o protocolo de teletransporte quântico
qubits, m_c, m_a = teleporte(np.kron(mensagem, alice_bob))
# Extrai o resultado do qubit do bob
index = int(f"{m_c}{m_a}0", 2)
teleportada = qubits[index : index + 2]
assert np.allclose(teleportada, mensagem)
# verificar_teleporte() # <- Para testar o protocolo de teletransporte
Implementação do Protocolo de Teletransporte usando Ket#
def teleporte(mensagem: ket.Quant, alice_bell: ket.Quant, bob_bell: ket.Quant):
"""
Realiza o protocolo de teletransporte quântico para transferir um estado
quântico de um qubit para outro.
Parâmetros:
mensagem (ket.Quant): O qubit que contém a mensagem a ser
teletransportada.
alice_bell (ket.Quant): O qubit entrelaçado com o qubit de Alice.
bob_bell (ket.Quant): O qubit entrelaçado com o qubit de Bob.
Retorna:
None: Esta função não retorna nada. Os resultados são diretamente
aplicados aos qubits de Bob.
"""
# Aplica a porta CNOT no qubit da mensagem controlado pelo qubit de Alice
ket.CNOT(mensagem, alice_bell)
# Aplica a porta Hadamard no qubit da mensagem
ket.H(mensagem)
# Realiza as medidas nos qubits da mensagem e de Alice
m_c = ket.measure(mensagem)
m_a = ket.measure(alice_bell)
# Aplica as correções necessárias nos qubits de Bob com base nas medidas
if m_a.get() == 1:
ket.X(bob_bell)
if m_c.get() == 1:
ket.Z(bob_bell)
# Cria um processo quântico
p = ket.Process()
# Aloca um qubit para a mensagem e prepara seu estado
mensagem = p.alloc()
ket.H(mensagem)
ket.T(mensagem)
# Armazena o estado atual da mensagem antes do teletransporte
mensagem_dump = ket.dump(mensagem)
# Aloca os qubits entrelaçados de Alice e Bob
alice_bell = p.alloc()
bob_bell = p.alloc()
# Prepara o estado entrelaçado entre Alice e Bob
ket.CNOT(ket.H(alice_bell), bob_bell)
# Realiza o protocolo de teletransporte quântico
teleporte(mensagem, alice_bell, bob_bell)
# Armazena o estado atual do qubit de Bob após o teletransporte
bob_dump = ket.dump(bob_bell)
# Imprime os resultados do teletransporte
print(f"Mensagem original:\n{mensagem_dump.show()}")
print(f"Mensagem teleportada:\n{bob_dump.show()}")
Mensagem original:
<IPython.core.display.Math object>
Mensagem teleportada:
<IPython.core.display.Math object>
Conclusão#
Os postulados apresentados neste capítulo são fundamentais para entender e avaliar a computação quântica. Eles formam a base teórica que nos permite descrever e analisar sistemas quânticos fechados, e são essenciais para o desenvolvimento e compreensão de algoritmos quânticos.
A implementação prática dos conceitos discutidos neste capítulo nos permitiu manipular estados quânticos, realizar operações com portas quânticas e até mesmo simular protocolos quânticos como o teletransporte quântico. Ao escrever e executar código, ganhamos uma intuição valiosa sobre como os conceitos teóricos se traduzem em operações práticas em um computador quântico ou simulador.
Embora o uso do NumPy seja adequado para experimentos iniciais e circuitos simples, é importante ressaltar que conforme a complexidade do circuito aumenta, a escalabilidade pode se tornar um desafio. Nesse contexto, recomendamos a transição para plataformas mais especializadas, como a biblioteca Ket. Ao utilizar a biblioteca Ket, é possível simplificar e abstrair grande parte das operações necessárias para manipular estados quânticos, tornando o desenvolvimento e a execução de circuitos quânticos mais eficientes e escaláveis.
Sandbox#
Gostaria de praticar um pouco mais os conceitos abordados neste capítulo? Utilize a caixa de código abaixo para realizar experimentos adicionais, explorar variações nos circuitos ou testar implementações específicas. Essa abordagem prática proporciona uma oportunidade valiosa para aprofundar seu conhecimento e ganhar confiança na manipulação de qubits.
print("Use esse espaço para explorar um pouco mais os conceitos visto nesse capitulo.")
Use esse espaço para explorar um pouco mais os conceitos visto nesse capitulo.