Hola mis queridos programming motherfuckers, hoy vamos a ver cómo se programa un ordenador cuántico, y además vamos a desarrollar un pequeño programa juntos, desde cero, y todo ello usando únicamente herramientas gratuitas.

Lo primero que debemos saber es que los ordenadores cuánticos no tienen capacidad para ejecutar programas por sí mismos sino que en realidad son un tipo de hardware que se usa desde ordenadores convencionales, algo parecido a las tarjetas gráficas. Y al igual que una tarjeta gráfica por sí misma no puede ejecutar un juego sino que necesitamos que sea un ordenador el que ejecute el juego y que use la tarjeta para acelerar los gráficos, a los ordenadores cuánticos les ocurre algo parecido.

Esto significa que el hardware cuántico se suele usar y programar desde ordenadores convencionales usando lenguajes de programación clásicos, como por ejemplo Python, y frameworks que permiten acceder e interactuar con este tipo de hardware. Exactamente igual que hacemos con los SDKs que nos permiten acceder y utilizar al hardware de las aceleradoras gráficas.

Y si vuestra pregunta es ¿qué frameworks hay disponibles para usar hardware cuántico? os diré que a día de hoy hay bastantes alternativas aunque parece que los dos que más se oyen son Cirq y Qiskit. Ambos dos se programan desde Python.

Hoy vamos a usar Qiskit, pero prometo hacer otro vídeo usando Cirq.

Pues bien, Qiskit se puede instalar con el comando

pip install qiskit

Una vez tenemos instalado Qiskit, necesitaremos un entorno de desarrollo. Lo más cómodo es usar Jupyter que permite ir trasteando el código de una manera súper visual.

Jupyter se instala también con pip así:

pip install jupyter notebook jupyterlab --upgrade

Y ahora ya estamos en disposición de lanzar nuestro entorno de desarrollo así:

jupyter lab

Lo primero será ver si qiskit lo tenemos bien instalado y funciona. Para ello tan fácil como escribir

import qiskit
qiskit.qiskit_version

Y deberemos obtener un mensaje como este:

{'qiskit-terra': '0.18.2', 'qiskit-aer': '0.8.2', 'qiskit-ignis': '0.6.0', 'qiskit-ibmq-provider': '0.16.0', 'qiskit-aqua': '0.9.5', 'qiskit': '0.29.1', 'qiskit-nature': None, 'qiskit-finance': None, 'qiskit-optimization': None, 'qiskit-machine-learning': None}

¡Enhorabuena! si ves esto significa que lo tienes todo bien instalado. Aquí podemos ver la versión de los distintos componentes que integran qiskit.

Ahora vamos a realizar nuestro primer programa. ¿Estáis emocionados? ¿Eim? ¿Es vuestra primera vez?

Bueno, lo primero será importar de la librería qiskit el componente que se usa para definir circuitos cuánticos, así:

from qiskit import QuantumCircuit

En los ordenadores cuánticos no programamos algoritmos, sino circuitos.
La diferencia entre un algoritmo y un circuito es que el algoritmo permite definir una lógica de control que determine el orden de ejecución de las instrucciones de nuestro programa, y el circuito no. Es decir, en un algoritmo podemos por ejemplo iterar bucles y llamar a funciones. En los circuitos todo se ejecuta secuencialmente de principio a fin, como una partitura musical. Un circuito es como un chip que tiene un conjunto de entradas y de salidas, de forma que al aplicar electricidad en las entradas, esa electricidad se propaga por un conjunto de puertas lógicas hasta llegar a las salidas, describiendo así una función que dará un resultado u otro dependiendo de las entradas que se apliquen.
Los circuitos cuánticos son exactamente iguales, pero en lugar de utilizar bits y puertas lógicas, utiliza cubits y puertas cuánticas.

Y justo por esto, para ejecutar algoritmos cuánticos necesitaremos un ordenador convencional que nos permita ejecutar la parte algorítmica de forma que será desde esos algoritmos desde donde podremos invocar circuitos cuánticos de la misma forma que invocamos funciones o llamamos a APIs.

¿Veis la foto? El ordenador convencional lo vamos a seguir necesitando de momento sí o sí para programar nuestros programas cuánticos. Y usaremos los ordenadores cuánticos para definir los circuitos que invocaremos desde nuestros programas. Esa es la idea.

Así que indicamos que vamos a definir un circuito así:

from qiskit import QuantumCircuit
qc = QuantumCircuit(1)

Así definimos un circuito cuántico formado por un solo cubit.

¿qué es un cubit?

En mi video sobre computación cuántica y en mi otro video sobre el entrelazamiento cuántico explicó con detalle que son los cubits.

El resumen es que mientras los ordenadores convencionales usan bits, que pueden valer 0 o 1, los ordenadores cuánticos usan cubits que pueden valer 0, 1, y 0 y 1 a la vez.

Cuando un cubit vale 0 y 1 a la vez se dice que está en superposición. Y recordad que cuando leemos el valor de un cubit, éste colapsa y deja de estar en superposición y por lo tanto pasa a valer 0 o 1, igual que un bit.

Los distintos valores que va adoptando un cubit a medida que se va operando con él es lo que se conoce como su estado, y es típico representarlo mediante una esfera llamada esfera de Bloch.

La esfera de Bloch

La esfera de Bloch es una esfera con un vector en su interior que puede apuntar en cualquier dirección. La dirección del vector es lo que representa el estado del cubit. Si el vector apunta hacia arriba el cubit vale 0, y si apunta hacia abajo él cubit vale 1, pero cualquier otra dirección a la que apunte representará un estado de superposición, es decir, cuando decimos que vale 0 y 1 a la vez.

Lo que ocurre es que no vale 0 y 1 en la misma proporción necesariamente. Si tiramos una moneda al aire, tiene las mismas probabilidades de salir cara y cruz, pero en los cubits esto no es así necesariamente. A veces ocurre que vale 90% 0 y 10% 1, que quiere decir que tendrá un 90% de posibilidades de colapsar a 0 y un 10% de colapsar a 1. O puede valer 30% 0 y 70% 1, o 50% 0 y 50% 1.
Lo podemos ver como un dado de n caras, donde en unas caras hay un 0 y en otras un 1. Cuantas más caras tengan un 0, más probabilidad tendremos de sacar un 0 al tirar el dado.

En la esfera de Bloch ocurre que cuando el vector está más próximo al 0 más probabilidades tendrá el cubit de colapsar a 0 y viceversa. Cuanto más próximo esté el vector al 1 más probabilidades tendrá el cubit de colapsar a 1.

Por este motivo, si el vector está a la misma distancia del 0 y del 1, es decir, formando un ángulo de 90 grados con respecto al eje z, tendrá las mismas probabilidades de colapsar al 0 y al 1.

Entonces, para representar la posición del vector dentro de la esfera, que a su vez representa el estado del cubit, se usa una matriz de 2 componentes, de forma que el primer componente indica la probabilidad de colapsar a 0, y el segundo componente indica la probabilidad de colapsar a 1.

Por ejemplo, si el cubit vale 0, la matriz sería [1, 0], indicando el primer componente de la matriz, que vale 1, la probabilidad de colapsar a 0, y el segundo componente de la matriz, que es 0, la probabilidad de colapsar a 1. En este caso, la probabilidad de colapsar a 0 es absoluta (1 es equivalente a un 100% de probabilidades) y la de colapsar a 1 es 0 o nula. .
En realidad los componentes de esta matriz no son probabilidades, sino que representan amplitudes. Lo que sucede es que estas amplitudes son en realidad proporcionales a las probabilidades de colapsar a 0 o a 1, por lo que hablaremos de probabilidades para no liarnos, pero que sepáis que técnicamente representan amplitudes.

(img)

Por el contrario, sí el cubit vale 1 la matriz sería [0,1], es decir, el primer componente de la matriz, que vale 0, indica la probabilidad de colapsar a 0, y el segundo componente de la matriz, que vale 1, indica la probabilidad de colapsar a 1. En este caso la probabilidad de colapsar a 0 es nula y la de colapsar a 1 es absoluta.

¿Lo vais viendo?

En nuestro código si queremos que el cubit de nuestro circuito se inicialice con el valor 1, lo expresaríamos así:

from qiskit import QuantumCircuit
qc = QuantumCircuit(1)

# Define initial_state as |1⟩
initial_state = [0, 1]

# Apply initialization operation to the qubit at position 0
qc.initialize(initial_state, 0)

En general, para representar el estado de un cubit usaremos una matriz con dos valores α y β, de forma que α y β pueden ser un valor entre 0 y 1 que indican la probabilidad del cubit de colapsar a 0 y a 1 respectivamente.

Y además, y muy importante, α y β han de cumplir que:

Por culturilla general: existe una notación llamada Dirac que permite expresar los estados [1, 0] como |0⟩ y [0, 1] como |1⟩. Y generalizando, un estado dado arbitrario, se suele representar así |ψ⟩.

Resumen: Un cubit vale 0, 1 o 0 y 1 a la vez. Su estado se puede representar mediante una matriz de dos componentes, y cada componente indica la probabilidad del cubit de colapsar a 0 o a 1 cuando leamos su valor.

Quedaros con la copla, porque vamos a representar, y a operar con cubits usando para ello las matrices que representan sus estados, por lo que las operaciones con cubits las vamos a representar como operaciones sobre matrices, sobre las matrices que representan los estados de los cubits.

Y ¿Qué pasa si en lugar de un cubit, tenemos dos?
Pues muy fácil, dos bits pueden valer 00, 01, 10 o 11.
Sin embargo, dos cubits pueden valer 00, 01, 10, 11, o ¡los 4 valores a la vez!

Y de igual forma que usamos una matriz de dos componentes para indicar el estado de un cubit usaremos una matriz de cuatro componentes para indicar el estado de dos cubits, y en realidad todo es análogo a lo que sucede con un cubit.

Si tuviésemos 4 cubits, estos podrían valer cualquiera de las 16 combinaciones binarias que se pueden formar con 4 bits, y las 16 combinaciones a la vez. Y necesitaríamos una matriz de 16 componentes para representarlos. Lo vais viendo. Y así sucesivamente.

En general, para un número n de cubits estos pueden valer cualquiera de las 2n combinaciones así como las 2n combinaciones a la vez.
Por ejemplo, para 64 cubits, estos pueden valer cualquiera de las 264 combinaciones, es decir, un total de 18.446.744.073.709.551.616 de combinaciones, y también puede valer los 18.446.744.073.709.551.616 de combinaciones a la vez. Y para representarlo se usaría una matriz de 18.446.744.073.709.551.616 valores, de forma que cada valor puede valer un número entre 0 y 1.

¿vais viendo la magnitud de los números que podemos manejar con un número relativamente pequeño de cubits? ¿Vais intuyendo ya porque los métodos de cálculo con los ordenadores cuánticos son aplastantemente más potentes que con los ordenadores convencionales?

La pregunta ahora es ¿qué ventaja ofrece un circuito cuántico? ¿qué es lo que nos aporta que hace que los ordenadores cuánticos estén cogiendo tantísima relevancia?

Bueno, estáis a punto de entenderlo, pero para ello vamos a ver un par de puertas cuánticas.

Puertas cuánticas

Una puerta cuántica es un operador que actúa sobre uno o más cubits y que  permite alterar sus estados a nuestra conveniencia.

En este artículo vamos a ver dos de las puertas cuánticas mas usadas: La puerta NOT y la puerta Hadamard.

La puerta NOT es super simple de entender. Lo que hace es invertir el valor de un cubit. Por ejemplo, si un cubit vale 0, pasa a valer 1. Y si un cubit vale 1, pasa a valer 0.
Si vemos el cubit en su forma de matriz, y recordad que en su forma de matriz el primer elemento indica la probabilidad de colapsar a cero y el segundo la probabilidad de colapsar a 1, el operador NOT intercambia los valores de la matriz, que es equivalente a intercambiar las probabilidades de colapsar a uno u otro valor, de forma que tras la operación, la probabilidad de colapsar a 0 pasa a ser la de colapsar a 1, y viceversa: la probabilidad de colapsar a 1 pasa a ser la de colapsar a 0. Vamos, se invierten las probabilidades, o lo que es lo mismo, se intercambian los valores de la matriz.

La puerta NOT se suele ver representada en un circuito como una X sobre la linea del qubit sobre el que actúa, y se representa matemáticamente mediante esta matriz:

De forma que aplicar una puerta NOT sobre un cubit es equivalente a multiplicar la matriz NOT por la matriz de estado del cubit, que da como resultado una nueva matriz de estado pero con los valores intercambiados, es decir, el valor de arriba pasa a ser el de abajo y el valor de abajo pasa a ser el de

La puerta Hadamard es especialmente útil porque nos permite poner un cubit en superposición.

La puerta Hadamard se suele ver representada en un circuito como una H sobre la linea del qubit sobre el que actúa, y se representa matemáticamente mediante la siguiente matriz:

Y aplicar una puerta Haramard sobre un cubit es equivalente a multiplicar esta matriz por la matriz de estado de ese cubit:

En realidad es simple, aunque la representación matemática puede asustar un poco al principio. Pero si lo vemos mediante la esfera de Bloch, que para eso está, lo vais a entender en seguida.

Si el cubit vale 0, al aplicar el operador Hadamard, el cubit pasa a estar en superposición, de forma que el vector está a la misma distancia del 0 y del 1, con lo que tiene las mismas probabilidades de colapsar a 0 y a 1.
Si el cubit vale 1, al aplicar el operador Hadamard, el cubit pasa igualmente a estar en superposición, con las mismas probabilidades de colapsar a 0 y a 1.

Eso es lo mismo que sucede si multiplicamos la matriz de Hadamard por la matriz que representa el |0⟩

Como veis, ambos componentes valen igual, que indica que tienen la misma probabilidad de colapsar a 0 y a 1.

Ah, que no os esperabais este valor? Y cual os esperabais acaso, este? [0.5, 0.5]
No hombre, recordad que os he dicho antes que los valores de la matriz han de cumplir que la suma de sus cuadrados sea 1, porque no son probabilidades sino amplitudes, y aunque son proporcionales a las probabilidades de colapsar a cada valor, no dejan de ser valores que han de cumplir esta relación matemática.

Por lo que si queremos que ambos valores valgan lo mismo, el único valor que cumple esto es 1/sqrt(2).

¿Lo vais viendo?

Existen muchas más puertas cuánticas que nos permiten manipular de distintas formas el estado de los cubits, pero en realidad todas ellas se presentan como matrices y aplicarlas sobre cubits es equivalente a multiplicar esas matrices.

Así que generalizando: Las puertas cuánticas y los cubits se representan con matrices de forma que aplicar puertas cuánticas a cubits es equivalente a multiplicar esas matrices.

Y diréis, ¡vaya! si los cálculos cuánticos se reducen a multiplicar matrices, ¿qué aportan entonces que no aporten ya los ordenadores o las aceleradoras gráficas? Qué tengo que recordar que las aceleradoras gráficas son extremadamente óptimas multiplicando matrices.

Pues vereis, mis queridos programming motherfuckers, multiplicar matrices requiere mucho, pero que mucho gasto computacional, y cuanto más grandes son estas matrices más crece y de forma exponencial el número de cálculos que se requerirán para resolver esas multiplicaciones.

De hecho, la mayoría de los ordenadores convencionales del mundo mundial, incluido el tuyo, no es capaz de multiplicar matrices de tamaños relativamente pequeños.

Y aquí es donde vienen los ordenadores cuánticos al rescate. Un ordenador cuántico es capaz de multiplicar matrices en un solo paso y sin que importe el tamaño de esas matrices. Eso es lo que aportan, que son capaces de resolver cálculos en tiempos razonables que los ordenadores convencionales no pueden.

¿Lo veis ya? Bueno, pues ahora vamos a continuar con nuestro programa.

from qiskit import QuantumCircuit
qc = QuantumCircuit(1)

# Define initial_state as |1⟩
initial_state = [0, 1]

# Apply initialization operation to the qubit at position 0
qc.initialize(initial_state, 0)

Tenemos un cubit en nuestro circuito iniciado como 1. Si lo queremos ver representado en una esfera de Bloch, podemos escribir:

from qiskit import QuantumCircuit, Aer, executefrom qiskit.visualization import plot_bloch_multivectorfrom mpl_toolkits import mplot3d

qc = QuantumCircuit(1)initial_state = [0, 1]
qc.initialize(initial_state, 0)

from qiskit.visualization import plot_bloch_multivector

out = execute(qc,Aer.get_backend('statevector_simulator')).result().get_statevector()
plot_bloch_multivector(out)

Como podemos apreciar, el vector apunta al uno, que es el valor con el que hemos inicializado el qubit.
Si ahora aplicamos una puerta NOT sobre el cubit, cambiaremos el estado del cubit de 1 a 0, mirad:

qc.x(0)
out = execute(qc,Aer.get_backend('statevector_simulator')).result().get_statevector()plot_bloch_multivector(out)

Y si lo ponemos en superposición con una puerta Hadamard, así:

qc.h(0)
out = execute(qc,Aer.get_backend('statevector_simulator')).result().get_statevector()
plot_bloch_multivector(out)

Podemos observar como ahora el vector no apunta ni al cero ni al uno, sino que forma un ángulo recto con respecto al eje z, de forma que en este estado tendrá las mismas posibilidades de colapsar al 0 y al 1.

Si quisiéramos ver representado en una gráfica las probabilidades de colapsar al 0 y al 1, pondríamos esto:

qc.measure_all()
results = execute(qc,Aer.get_backend('qasm_simulator')).result().get_counts()
plot_histogram(results)

Esta gráfica sale como resultado de haber ejecutado este circuito muchas veces y por cada una anotar si colapsa al 0 o al 1. De esta forma la barra de la izquierda es proporcional al número de veces que ha colapsado al 0, y la de la derecha al número de veces que ha colapsado al 1. Como ambas barras tienen más o menos la misma altura, esto significa que el cubit tiene la misma probabilidad de colapsar al 0 y al 1, de lo que se puede deducir que el cubit estaría en superposición y apuntando al ecuador de la esfera de Bloch.

En cualquier momento podemos ver el circuito que estamos configurando con esta instrucción:

qc.draw(output='mpl')

Como podemos apreciar, tenemos un circuito que se ha inicializado a 1, luego hemos aplicado una puerta NOT que ha cambiado el estado de 1 a 0, luego hemos aplicado una puerta Hadamard que nos ha puesto en superposición nuestro cubit, y finalmente hemos leído el resultado.

A qué mola, ¿eh?

Antes os he dicho que una puerta Hadamard se representa como esta matriz, que multiplicada por los componentes que representan el estado del qubit nos da como resultado un estado nuevo, el resultante de aplicar la puerta Hadamard.

Entonces, si el estado 0 se representa como el vector [1, 0], que es el estado sobre el que hemos aplicado la puerta Hadamard en nuestro circuito, esto en realidad es equivalente a haber multiplicado esta matriz por [1, 0], dándonos como resultado esta otra matriz:

Con lo cual, en teoría, si inicializamos un cubit a 1/sqrt(2) [1, 1], dicho cubit quedaría en superposición y con la misma probabilidad de colapsar a 0 y a 1 exactamente igual que si hubiesemos aplicado una puerta Hadamard sobre un cubit en estado 0, no?

Pues veámoslo en código:

from math import sqrt

qc = QuantumCircuit(1)initial_state = [1/sqrt(2), 1/sqrt(2)]
qc.initialize(initial_state, 0)

from qiskit.visualization import plot_bloch_multivector

out = execute(qc,Aer.get_backend('statevector_simulator')).result().get_statevector()
plot_bloch_multivector(out)

Y efectivamente, hemos obtenido la misma esfera. Como podeis apreciar, inicializar directamente un cubit a este valor nos pone igualmente en superposición nuestro cubit apuntando igualmente al ecuador de la esfera de Bloch, o lo que es lo mismo, sitúa el estado del cubit con la misma probabilidad de colapsar al 0 y al 1.

Por lo que en definitiva, aplicar puertas cuánticas sobre cubits es equivalente a multiplicar las matrices que representan dichas puertas sobre las matrices que representan los estados de estos cubits.

Bueno, pues así es como se programa un ordenador cuántico, inicializando cubits, aplicando puertas cuánticas y leyendo sus resultados. Todo ello orquestado desde un lenguaje que es procesado en un ordenador convencional en comunicación con el ordenador cuántico.

Hoy hemos usado un simulador de ordenador cuántico, concrétamente statevector_simulator.
Si hubiésemos querido ejecutar nuestro código en un ordenador cuántico real tendríamos que haber indicado aquí el identificador del ordenador cuántico en cuestión. En el video Así funciona la seguridad y así la rompe la computación cuántica probamos un algoritmo cuántico, concretamente Shor, en un ordenador cuántico real de 15 cubits. Si no lo habéis visto os lo recomiendo.

Bueno, pues esto ha sido todo. Estoy preparando nuevos artículos y videos donde entraremos más en calor en este apasionante mundo de la computación cuántica.

Y lo de siempre: Si te ha gustado este artículo y en general te gustan todos estos temas no olvides suscribirte (que es gratis!)
Si compartes el vídeo en tus redes me estarás ayudando a fomentar el canal, y eso hará que cada vez saque vídeos más guapos y con mayor frecuencia.

Nos vemos
Chao!

Comparte esta publicación