Pular para o conteúdo principal

Algoritmo de Deutsch

O algoritmo de Deutsch resolve o problema de paridade para o caso especial em que n=1.n = 1. No contexto da computação quântica, esse problema é às vezes chamado de problema de Deutsch, e seguiremos essa nomenclatura nesta lição.

Para ser preciso, a entrada é representada por uma função f:ΣΣf:\Sigma \rightarrow \Sigma de um bit para um bit. Existem quatro funções desse tipo:

af1(a)0010af2(a)0011af3(a)0110af4(a)0111\rule[-10mm]{0mm}{10mm} \begin{array}{c|c} a & f_1(a)\\ \hline 0 & 0\\ 1 & 0 \end{array} \qquad \begin{array}{c|c} a & f_2(a)\\ \hline 0 & 0\\ 1 & 1 \end{array} \qquad \begin{array}{c|c} a & f_3(a)\\ \hline 0 & 1\\ 1 & 0 \end{array} \qquad \begin{array}{c|c} a & f_4(a)\\ \hline 0 & 1\\ 1 & 1 \end{array}

A primeira e a última dessas funções são constantes, e as duas do meio são balanceadas, o que significa que os dois possíveis valores de saída da função ocorrem o mesmo número de vezes conforme variamos as entradas. O problema de Deutsch consiste em determinar a qual dessas duas categorias a função de entrada pertence: constante ou balanceada.

Problema de Deutsch

Entrada: uma função f:{0,1}{0,1}f:\{0,1\}\rightarrow\{0,1\}
Saída: 00 se ff é constante, 11 se ff é balanceada

Se interpretarmos a função de entrada ff no problema de Deutsch como representando acesso aleatório a uma string, estamos pensando em uma string de dois bits: f(0)f(1).f(0)f(1).

functionstringf100f201f310f411\begin{array}{cc} \mathsf{function} & \mathsf{string}\\ \hline f_1 & 00 \\ f_2 & 01 \\ f_3 & 10 \\ f_4 & 11 \end{array}

Visto dessa forma, o problema de Deutsch consiste em calcular a paridade (ou, equivalentemente, o OU-exclusivo) dos dois bits.

Todo algoritmo de consulta clássico que resolve corretamente esse problema precisa consultar os dois bits: f(0)f(0) e f(1).f(1). Se soubermos que f(1)=1,f(1) = 1, por exemplo, a resposta ainda pode ser 00 ou 11, dependendo se f(0)=1f(0) = 1 ou f(0)=0f(0) = 0, respectivamente. Todo outro caso é semelhante; conhecer apenas um dos dois bits não fornece nenhuma informação sobre a paridade deles. Portanto, o circuito booleano descrito na seção anterior é o melhor que podemos fazer em termos do número de consultas necessárias para resolver esse problema.

Descrição do circuito quântico

O algoritmo de Deutsch resolve o problema de Deutsch usando uma única consulta, fornecendo assim uma vantagem quantificável da computação quântica sobre a clássica. Essa pode ser uma vantagem modesta — uma consulta em vez de duas — mas precisamos começar em algum lugar. Avanços científicos às vezes têm origens aparentemente modestas.

Aqui está um circuito quântico que descreve o algoritmo de Deutsch:

Algoritmo de Deutsch

Análise

Para analisar o algoritmo de Deutsch, vamos percorrer a ação do circuito acima e identificar os estados dos qubits nos momentos sugeridos por esta figura:

Estados durante o algoritmo de Deutsch

O estado inicial é 10,\vert 1\rangle \vert 0 \rangle, e as duas operações de Hadamard no lado esquerdo do circuito transformam esse estado em

π1=+=12(01)0+12(01)1.\vert \pi_1 \rangle = \vert - \rangle \vert + \rangle = \frac{1}{2} \bigl( \vert 0\rangle - \vert 1\rangle \bigr) \vert 0\rangle + \frac{1}{2} \bigl( \vert 0\rangle - \vert 1\rangle \bigr) \vert 1\rangle.

(Como sempre, seguimos a convenção de ordenação de qubits do Qiskit, que coloca o qubit do topo à direita e o qubit de baixo à esquerda.) Pode parecer pouco intuitivo escrever esse estado de produto parcialmente distribuído (deixando os estados do qubit 1 fatorados fora), mas isso tornará nossas expressões posteriores mais compactas.

Em seguida, a porta UfU_f é aplicada. De acordo com a definição da porta UfU_f, o valor da função ff para o estado clássico do qubit do topo/mais à direita é aplicado via XOR no qubit de baixo/mais à esquerda, o que transforma π1\vert \pi_1\rangle no estado

π2=12(0f(0)1f(0))0+12(0f(1)1f(1))1.\vert \pi_2 \rangle = \frac{1}{2} \bigl( \vert 0 \oplus f(0) \rangle - \vert 1 \oplus f(0) \rangle \bigr) \vert 0 \rangle + \frac{1}{2} \bigl( \vert 0 \oplus f(1) \rangle - \vert 1 \oplus f(1) \rangle \bigr) \vert 1 \rangle.

Podemos simplificar essa expressão observando que a fórmula

0a1a=(1)a(01)\vert 0 \oplus a\rangle - \vert 1 \oplus a\rangle = (-1)^a \bigl( \vert 0\rangle - \vert 1\rangle \bigr)

é válida para ambos os possíveis valores aΣ.a\in\Sigma. De forma mais explícita, os dois casos são os seguintes.

0010=01=(1)0(01)0111=10=(1)1(01)\begin{aligned} \vert 0 \oplus 0\rangle - \vert 1 \oplus 0\rangle & = \vert 0 \rangle - \vert 1 \rangle = (-1)^0 \bigl( \vert 0\rangle - \vert 1\rangle \bigr)\\ \vert 0 \oplus 1\rangle - \vert 1 \oplus 1\rangle & = \vert 1 \rangle - \vert 0\rangle = (-1)^1 \bigl( \vert 0\rangle - \vert 1\rangle \bigr) \end{aligned}

Assim, podemos expressar π2\vert\pi_2\rangle alternativamente da seguinte forma:

π2=12(1)f(0)(01)0+12(1)f(1)(01)1=((1)f(0)0+(1)f(1)12).\begin{aligned} \vert\pi_2\rangle & = \frac{1}{2} (-1)^{f(0)} \bigl( \vert 0 \rangle - \vert 1 \rangle \bigr) \vert 0 \rangle + \frac{1}{2} (-1)^{f(1)} \bigl( \vert 0 \rangle - \vert 1 \rangle \bigr) \vert 1 \rangle \\ & = \vert - \rangle \biggl( \frac{(-1)^{f(0)} \vert 0\rangle + (-1)^{f(1)} \vert 1\rangle}{\sqrt{2}}\biggr). \end{aligned}

Algo interessante acabou de acontecer! Embora a ação da porta UfU_f sobre estados da base padrão deixe o qubit do topo/mais à direita intacto e aplique o XOR do valor da função no qubit de baixo/mais à esquerda, aqui vemos que o estado do qubit do topo/mais à direita mudou (em geral), enquanto o estado do qubit de baixo/mais à esquerda permanece o mesmo — especificamente no estado \vert - \rangle antes e depois da aplicação da porta UfU_f. Esse fenômeno é conhecido como phase kickback (retrocesso de fase), e falaremos mais sobre ele em breve.

Com uma simplificação final, que consiste em extrair o fator (1)f(0)(-1)^{f(0)} para fora da soma, obtemos esta expressão para o estado π2\vert\pi_2\rangle:

π2=(1)f(0)(0+(1)f(0)f(1)12)={(1)f(0)+se f(0)f(1)=0(1)f(0)se f(0)f(1)=1.\begin{aligned} \vert\pi_2\rangle & = (-1)^{f(0)} \vert - \rangle \biggl( \frac{\vert 0\rangle + (-1)^{f(0) \oplus f(1)} \vert 1\rangle}{\sqrt{2}}\biggr) \\ & = \begin{cases} (-1)^{f(0)} \vert - \rangle \vert + \rangle & \text{se $f(0) \oplus f(1) = 0$}\\[1mm] (-1)^{f(0)} \vert - \rangle \vert - \rangle & \text{se $f(0) \oplus f(1) = 1$}. \end{cases} \end{aligned}

Observe que nessa expressão temos f(0)f(1)f(0) \oplus f(1) no expoente de 1-1, em vez de f(1)f(0),f(1) - f(0), que seria o esperado de um ponto de vista puramente algébrico, mas obtemos o mesmo resultado de qualquer forma. Isso ocorre porque o valor (1)k(-1)^k para qualquer inteiro kk depende apenas de kk ser par ou ímpar.

Aplicando a porta de Hadamard final ao qubit do topo, chegamos ao estado

π3={(1)f(0)0se f(0)f(1)=0(1)f(0)1se f(0)f(1)=1,\vert \pi_3 \rangle = \begin{cases} (-1)^{f(0)} \vert - \rangle \vert 0 \rangle & \text{se $f(0) \oplus f(1) = 0$}\\[1mm] (-1)^{f(0)} \vert - \rangle \vert 1 \rangle & \text{se $f(0) \oplus f(1) = 1$}, \end{cases}

o que leva ao resultado correto com probabilidade 11 quando o qubit da direita/topo é medido.

Observações adicionais sobre o phase kickback

Antes de prosseguir, vamos analisar a discussão acima sob um ângulo ligeiramente diferente, o que pode lançar alguma luz sobre o fenômeno do phase kickback.

Primeiro, observe que a seguinte fórmula é válida para todas as escolhas de bits b,cΣ.b,c\in\Sigma.

bc=Xcb\vert b \oplus c\rangle = X^c \vert b \rangle

Isso pode ser verificado checando os dois possíveis valores c=0c = 0 e c=1c = 1:

b0=b=Ib=X0bb1=¬b=Xb=X1b.\begin{aligned} \vert b \oplus 0 \rangle & = \vert b\rangle = \mathbb{I} \vert b \rangle = X^0 \vert b \rangle\\ \vert b \oplus 1 \rangle & = \vert \neg b\rangle = X \vert b \rangle = X^1 \vert b \rangle. \end{aligned}

Usando essa fórmula, vemos que

Uf(ba)=bf(a)a=(Xf(a)b)aU_f \bigl(\vert b\rangle \vert a \rangle\bigr) = \vert b \oplus f(a) \rangle \vert a \rangle = \bigl(X^{f(a)}\vert b \rangle\bigr) \vert a \rangle

para toda escolha de bits a,bΣ.a,b\in\Sigma. Como essa fórmula é verdadeira para b=0b=0 e b=1b=1, vemos por linearidade que

Uf(ψa)=(Xf(a)ψ)aU_f \bigl( \vert \psi \rangle \vert a \rangle \bigr) = \bigl(X^{f(a)}\vert \psi \rangle\bigr) \vert a \rangle

para todos os vetores de estado de qubit ψ,\vert \psi\rangle, e portanto

Uf(a)=(Xf(a))a=(1)f(a)a.U_f \bigl( \vert - \rangle \vert a \rangle \bigr) = \bigl(X^{f(a)} \vert - \rangle \bigr) \vert a \rangle = (-1)^{f(a)} \vert - \rangle \vert a \rangle.

A chave que faz isso funcionar é que X=.X\vert - \rangle = - \vert - \rangle. Em termos matemáticos, o vetor \vert - \rangle é um autovetor da matriz XX com autovalor 1.-1.

Discutiremos autovetores e autovalores com mais detalhes na próxima lição sobre Estimativa de fase e fatoração, onde o fenômeno do phase kickback é generalizado para outras operações unitárias.

Lembrando que escalares se propagam livremente pelos produtos tensoriais, encontramos uma forma alternativa de raciocinar sobre como a operação UfU_f transforma π1\vert \pi_1\rangle em π2\vert \pi_2\rangle na análise acima:

π2=Uf(+)=12Uf(0)+12Uf(1)=((1)f(0)0+(1)f(1)12).\begin{aligned} \vert \pi_2 \rangle & = U_f \bigl( \vert - \rangle \vert + \rangle \bigr)\\ & = \frac{1}{\sqrt{2}} U_f \bigl(\vert - \rangle \vert 0\rangle \bigr) + \frac{1}{\sqrt{2}} U_f \bigl(\vert - \rangle \vert 1\rangle \bigr)\\ & = \vert - \rangle \biggl( \frac{(-1)^{f(0)} \vert 0\rangle + (-1)^{f(1)} \vert 1\rangle}{\sqrt{2}}\biggr). \end{aligned}

Implementação no Qiskit

Agora vamos ver como podemos implementar o algoritmo de Deutsch no Qiskit. Começaremos com uma verificação de versão e em seguida faremos as importações necessárias exclusivamente para esta implementação. Para as implementações de outros algoritmos que se seguem, faremos as importações necessárias separadamente em prol de maior modularidade.

# 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
from qiskit_aer import AerSimulator

Primeiro definiremos um circuito quântico que implementa uma porta de consulta para uma das quatro funções f1,f_1, f2,f_2, f3,f_3, ou f4f_4 de um bit para um bit descritas anteriormente. Como já mencionamos, a implementação de portas de consulta não é realmente parte do próprio algoritmo de Deutsch; aqui estamos essencialmente mostrando uma maneira de preparar a entrada, na forma de uma implementação em circuito de uma porta de consulta.

def deutsch_function(case: int):
# This function generates a quantum circuit for one of the 4 functions
# from one bit to one bit

if case not in [1, 2, 3, 4]:
raise ValueError("`case` must be 1, 2, 3, or 4.")

f = QuantumCircuit(2)
if case in [2, 3]:
f.cx(0, 1)
if case in [3, 4]:
f.x(1)
return f

Podemos ver como cada circuito se parece usando o método draw. Aqui está o circuito para a função f3.f_3.

display(deutsch_function(3).draw(output="mpl"))

Saída da célula de código anterior

Em seguida, criaremos o circuito quântico real para o algoritmo de Deutsch, substituindo a porta de consulta por uma implementação em circuito quântico fornecida como argumento. Em breve, vamos conectar um dos quatro circuitos definidos pela função deutsch_function que definimos anteriormente. Barreiras são incluídas para mostrar a separação visual entre a implementação da porta de consulta e o restante do circuito.

def compile_circuit(function: QuantumCircuit):
# Compiles a circuit for use in Deutsch's algorithm.

n = function.num_qubits - 1
qc = QuantumCircuit(n + 1, n)

qc.x(n)
qc.h(range(n + 1))

qc.barrier()
qc.compose(function, inplace=True)
qc.barrier()

qc.h(range(n))
qc.measure(range(n), range(n))

return qc

Novamente podemos ver como o circuito se parece usando o método draw.

display(compile_circuit(deutsch_function(3)).draw(output="mpl"))

Saída da célula de código anterior

Por fim, criaremos uma função que executa o circuito definido anteriormente uma única vez e retorna o resultado apropriado: "constant" ou "balanced."

def deutsch_algorithm(function: QuantumCircuit):
# Determine if a one-bit function is constant or balanced.

qc = compile_circuit(function)

result = AerSimulator().run(qc, shots=1, memory=True).result()
measurements = result.get_memory()
if measurements[0] == "0":
return "constant"
return "balanced"

Agora podemos executar o algoritmo de Deutsch em qualquer uma das quatro funções definidas acima.

f = deutsch_function(3)
display(deutsch_algorithm(f))
'balanced'