May 15, 2024 | 6 min read
Funciones y oráculos#
\( \newcommand{\bra}[1]{\langle #1|} \) \( \newcommand{\ket}[1]{|#1\rangle} \) \( \newcommand{\braket}[2]{\langle #1|#2\rangle} \) \( \newcommand{\i}{{\color{blue} i}} \) \( \newcommand{\Hil}{{\mathbb H}} \) \( \newcommand{\cg}[1]{{\rm C}#1} \)
Show code cell source
%run ../../macro_tQ.py
import sys
sys.path.append('../../')
import macro_tQ as tQ
Computación clásica universal#
En computación clásica, la únidad de información es el bit, una variable entera \(x = 0,1\) o, lo que es equivalente \(x \in {\mathbb Z}_2\). Las puertas lógicas elementales son,
sobre un bit: la puerta NOT \(x\to \neq x = x\oplus 1\), donde la suma es módulo 2.
sobre dos bits: las puertas AND, OR, XOR, y NAND se definen por las siguientes tablas de verdad
El teorema de computación clásica universal está en la base de la construcción de ordenadores de propósito general. El teorema de computación universal permite ver que cualquier circuito digital se puede construir mediante composición de las puertas mencionadas en esta lista.
De hecho, en realidad basta con una sola, que puede escogerse como la puerta \({\rm NAND}\)
Teorema: Computación digital universal
La puerta \({\rm NAND}\) es universal
las puertas \({\rm AND, OR}\) y \({\rm XOR}\) se pueden expresar en términos de ella
Surge la cuestión acerca de si la computación cuántica podrá contendrá la clásica actuando sobre simples cúbits \(\{\ket{0},\ket{1}\}\) (sin superposiciones)
Por ejemplo: la puerta clásica NOT es idéntica a las puerta cuántica X
Con respecto a las operaciones sobre dos bits, nos encontramos con una dificultad:
las puertas cuánticas son unitarias y, por tanto, reversibles.
pero las puertas clásicas son funciones del espacio de 2 bits a 1 bit. Por esta razón, ninguna de estas operaciones es invertible
Sin embargo si mantenemos memoria del primer registro, \(x\) la función XOR se vuelve invertible
No así las demás, que necesitan incluir un bit extra (auxiliar). Este formalismo, denominando, computación clásica reversible es una rama de la computación clásica.
Teorema:
Todas las puertas lógicas clásicas NOT, AND, OR, XOR y NAND admiten una implementación en un circuito cuántico en términos de la puerta de Toffoli (CCNOT)
La implementación de \(\hbox{NOT}\)
\(\rule{7mm}{0mm}\)es inmediata porque \({\rm CCNOT}\) aplicado a \(\ket{1,1,x}\) necesariamente actúa como \(X\) en el tercer bit.
La implementación de \(\hbox{AND}\)
\(\rule{7mm}{0mm}\)es directamente la acción de la puerta \(\hbox{CCNOT}\)
La implementación de \(\hbox{OR}\) se basa en el teorema de Morgan: \( x\lor y = \neg(\neg x\land \neg y)\, . \) De esta manera
\(\rule{7mm}{0mm}\)y el producto de operadores ejecuta la instrucción indicada en el miembro de la derecha.
La implementación de \(\hbox{XOR}\) se basa en que, actuando sobre \(\ket{1, x, y}\) \({\rm CCNOT}\) se reduce a \({\rm CNOT}\) actuando sobre \(\ket{x,y}\), que es precisamente \(\ket{x,x\oplus y}\).
La implementación de \(\hbox{NAND}\)
Preparación de un estado#
Muchos algoritmos cuánticos se benefician de la preparación de un estado inicial concreto
Separemos las amplitudes complejas en módulo y fase \(c_i = a_i e^{\gamma_i}\) donde \(a_i = |c_i|\).
Veamos el caso \(n=2\). El circuito que nos permite preparar un estado genérico es el siguiente
donde
El estado en la barrera, será
de donde obtenemos cuatro ecuaciones para cuatro incógnitas
sólo necesitamos 3 ángulos para representar 4 amplitudes debido a la ligadura \(\sum_i a_i^2 = 1\).
Una vez fijadas las amplitudes, la última parte del circuito es equivalente al operador unitario
Ejercicio
Diseña el circuito que inicializa un estado genérico de \(n=3\) cúbits. Úsalo para introducir el estado
Evidentemente, en el caso general, este circuito no puede ser eficiente puesto que es necesario aplicar al menos un número \(2*2^n\) de parámetros que involucran los coeficientes.
En algunos casos particulares que gozan de cierta simetría, sí que es posible encontrar un circuito eficiente. El caso extremo lo representa una superposición homogénea de elementos de la base
se obtiene con un circuito de coste = \(n~\) y \(~\) profundidad=1, la puerta de Walsh-Hadamard
Oráculos#
Una clase de problemas en los que la computación cuántica promete alcanzar una ventaja con respecto a la clásica se denominan algoritmos de interrogación de oráculo. Este tipo de problemas consiste en la clasificación de funciones clásicas atendiendo a alguna propiedad. Clásicamente la complejidad de este tipo de problemas crece exponencialmente porque exige examinar una fracción finita del número total de funciones.
Para poder establecer la ventaja potencial de un algoritmo cuántico sobre el clásico debemos ser capaces de evaluar dichas funciones sin desvelar cómo están hechas. Los operadores que implementan dichas funcionaes son cajas negras llamadas oráculos. Sólo nos está permitido invocarlas tantas veces como deseemos.
Generar un oráculo puede ser un problema de complejidad alta. Esta complejidad no forma parte del algoritmo de interrogación.
Funciones digitales#
Un computador clásico es capaz de ejecutar funciones digitales
descomponiendo la ejecución en puertas elementales. La construcción de \(f\) es equivalente a la especificación de \(m\) funciones \(f_1,f_2,...,f_m\) binarias
Es evidente que ninguna función binaria es invertible para \(n\geq 2\). Al igual que vimos anteriormente, para formular una función binaria como un circuito cuántico este hecho representa un inconveniente, debido a que éstos son, por naturaleza, invertibles.
Evaluación cuántica de funciones#
La manera más simple de fabricar, a partir de un mapa no invertible \(f\), un operador lineal \(U_f\), invertible, implica conservar los valores de la variables iniciales
Para \(f:\{0,1\}^n \to \{0,1\}\) necesitamos un total de \(n+1\) cúbits:
\(n\) cúbits que contienen el argumento de la función, \(\ket{x}_n \in \mathbb{C}^n\),
una ancilla que guardará el resultado, \(\ket{y} \in \mathbb{C}\).
Recordemos que, para definir un operador, debemos especificar cómo actúa sobre todos los elementos de una base. Sea \(\ket{x}\ket{y}\) una base de \(\Hil^{\otimes n+1}\). Asociaremos a la función \(f(x)\) el siguiente operador \(U_f\)
Donde \(\oplus\) indica suma módulo 2.
Ejercicio
prueba que \(U_f\) es unitario, además de ser hermítico.
>> Solución
Por un lado que \(U_f\) es su propio inverso \(U_f U_f =I\)
Es decir \(U_f = U_f^{-1}\). Para ver que es hermítico notemos que el operador \(U_f\) se escribe en la forma
con lo que
Definamos la nueva etiqueta \(z = y \oplus f(x) \Rightarrow y = z \oplus f(x)\). Entonces
Oráculo Booleano#
Si lo único que deseamos es codificar la función \(f(x)\) en una serie de cúbits, es suficiente con inicialiar la ancilla en el estado \(\ket{0}\)
Oráculos basados en esta codificación se denominan oráculos booleanos.
Oráculos de fase#
Nada nos impide inicializar la ancilla, en lugar de en el estado \(\ket{0}\), en el estado \(\ket{-}\)
Teorema:
los estados \(\ket{x}\otimes \ket{\pm}\) son autovectores de \(U_f\) son con autovalores \(+1\) y \((-1)^{f(x)}\) respectivamente
>> Prueba:
por un lado sabemos que los autovalores deben ser \(\pm 1\) dado que \(U_f^2 = I\). Veamos cada caso
Este teorema implica que
Vemos que se produce un típico efecto de retroceso de fase: especificando \(\ket{y} = \ket{-}\) codificamos \(f(x)\) en la fase \(~\to ~(-1)^{f(x)} = e^{i\pi f(x)}\)
Oráculos basados en esta codificación se denominan oráculos de fase
Construcción de funciones binarias#
Es muy sencillo establecer un método general para construir el circuito que implementa la función \(f: \{0, 1\}^n \rightarrow \{0, 1\}\) si conocemos la tabla completa de valores. Por ejemplo, consideremos la siguiente tabla de verdad para una función \(f: \{0, 1\}^3 \rightarrow \{0, 1\}\) concreta
Vamos a ver cómo construir el operador \(U_f\) en la codificación booleana.
La idea es considerar exclusivamente los términos que tienen como salida la variable 1, que denominaremos min-términos. Por ejemplo hay un min-término de la forma \(101 \to 1\) que se puede obtener mediante una puerta controlada como la siguiente
Cada min-término llevará asociada una puerta condicionada diferente. Su composición define la función \(f\) Para el caso de la tabla de verdad anterior, el circuito correspondiente vendrá dado por:
from qiskit import QuantumRegister, QuantumCircuit, ClassicalRegister
from qiskit.circuit.library import MCXGate
qr = QuantumRegister(4)
cr = ClassicalRegister(4)
qc = QuantumCircuit(qr, cr, name='q')
qc.append(MCXGate(3, ctrl_state=1), qr)
qc.append(MCXGate(3, ctrl_state=5), qr)
qc.append(MCXGate(3, ctrl_state=7), qr)
qc.draw(output='mpl')
/opt/anaconda3/envs/TalentQ/lib/python3.11/site-packages/qiskit/visualization/circuit/matplotlib.py:266: FutureWarning: The default matplotlib drawer scheme will be changed to "iqp" in a following release. To silence this warning, specify the current default explicitly as style="clifford", or the new default as style="iqp".
self._style, def_font_ratio = load_style(self._style)
donde hemos hecho uso de la puerta multicontrolada MCXGate de qiskit.
Para implementar una función digital \(f:\{0,1\}^n\to \{0,1\}^m\) lo único que tenemos que hacer es conocer la lista de salidas. La longitud en bits de las salidas será \(m\). El número de salidas será \(n\), con lo que, de la tabla podemos deducir todo lo que necesitamos.
Por ejemplo la siguiente tabla de verdad se corresponde con una función \(f:\{0,1\}^4\to \{0,1\}^4\), es decir, \(n=m=4\)
¡Vamos a programar!
def oracle(f_outputs):
n = int(np.log2(len(f_outputs))) #dimension del registro de entrada |x>
m = len(f_outputs[0]) #dimension del registro de salida |f(x)>
# print(m)
#generamos todos los posibles inputs en binario, completando con ceros hasta tener strings de n bits
inputs = [format(i, 'b').zfill(n) for i in range(2**n)]
# print(f_outputs[0])
qr_input = QuantumRegister(n, name='input')
qr_output = QuantumRegister(m, name='output')
qc = QuantumCircuit(qr_input, qr_output)
# Hacemos un bucle sobre los inputs
for i,input_str in enumerate(inputs):
ctrl_state= int(input_str[::],2)
# Para cada input, i, haz un bucle sobre cada bit del output
for j,output_bit in enumerate(f_outputs[i]):
# print(j,output)
if output_bit =='1':
qc.append(MCXGate(len(input_str), ctrl_state=ctrl_state),qr_input[:]+[qr_output[m-j-1]])
return qc
La función se especifica mediante una lista de strings binarios
f_out_1 = ['1111', '1011', '0011', '1000', '0101', '0100',
'0000', '1110', '0101', '0100', '0000', '1110',
'1111', '1011', '0011', '1000']
f_out_2= ['000', '001', '010', '011', '100', '101', '110', '111']
f_out_3= ['1', '1', '0', '0', '0', '1', '0', '0']
oracle(f_out_3).draw('mpl')
Evaluemos este circuito
n=3
qr_in = QuantumRegister(n, name='in')
qr_out = QuantumRegister(n, name='out')
cr = ClassicalRegister(n)
qc = QuantumCircuit(qr_in,qr_out,cr)
qc.x(2)
qc.append(oracle(f_out_2).to_gate(),qr_in[:]+qr_out[:])
qc.measure(qr_out,cr)
qc.draw('mpl')
from qiskit.primitives import Sampler
counts = Sampler().run(qc,shots=1).result().quasi_dists[0]
for keys,values in counts.items():
print("{0:b}".format(keys),values)
100 1.0
Ejercicio
Escribe una función balanced_binary(n) para \(f:S^n\to S\) tal que \(f(x) = \pm 1\) aleatoriamente y de forma equilibrada (es decir, tantos \(f(x)= +1\) como \(f(x)= -1\)).
>> Solución
def balanced_binary(n):
n_elementos = int(2**n)
valores = np.array([str(0) for _ in range(int(n_elementos/2))] + [str(1) for _ in range(int(n_elementos/2))])
np.random.shuffle(valores)
f_outputs = np.array([str(i) for i in valores])
return oracle(f_outputs)
Evidentemente la complejidad de este método para codificar una función binaria no escala bien porque exige una puerta condicionada por cada min-término. En general este número crecerá como una fracción del número de posibles entradas \(2^n\).
Para alguna función concreta, es posible que el circuito requiera un número de puertas que sólo sea proporcional a \(n\). Vamos a ver un caso así en el siguiente ejemplo
Funcion binaria lineal:#
Dados dos n-tuplas binarias \(x=(x_{n-1},\ldots,x_0)\) y \(a=(a_{n-1},\ldots,a_0)\) definimos la función lineal
donde \(\oplus\) es la suma módulo 2, por lo que el resultado sólo puede ser 0 ó 1.
El circuito que implementa esta función sólo requiere una puerta \(\hbox{CNOT}\) por cada valor de \(a_i=1\). Por ejemplo, tomemos \(a=01011\)
def linear_circuit(x,a):
assert(len(x)==len(a))
# Inicialización de los registros
qr_in = QuantumRegister(len(a), name='qr_in')
qr_out = QuantumRegister(1, name='qr_out')
cr = ClassicalRegister(1, name='cr')
qc = QuantumCircuit(qr_in, qr_out, cr, name='q_linear')
'inicializamo el estado x '
for i, xq in enumerate(reversed(x)): # ojo con la ordenación de qiskit, por eso está reversed()
if xq == '1':
qc.x(qr_in[i])
qc.barrier()
'codificamos la función lineal x.a '
for i, aq in enumerate(reversed(a)):
if aq == '1':
qc.cx(qr_in[i],qr_out)
qc.barrier()
qc.measure(qr_out[0],cr[0])
return qc
Veamos un ejemplo
a = '1011'
x = '1001'
qc=linear_circuit(x,a)
qc.draw('mpl')
La función \(a\cdot x = (1 + 0 + 0 + 1)mod(2) = 0\). Vamos a ver si este resultado es el hallado
n_tiradas = 1
counts = Sampler().run(qc,shots=1).result().quasi_dists[0]
print(counts)
{0: 1.0}
Ejercicio
sea la función \(f(x) = x^2\), evaluadad sobre el conjunto de valores \(x\in \{0,1,2,3\}\) . Halla la tabla de verdad en binario y construye el oráculo que implementa esta función.