
Tu primera aplicación cuántica
Hay una forma particularmente sencilla de escribir un primer programa cuántico: generar un número aleatorio, que, como veremos, tiene mucho que ver con el concepto de superposición. No vamos a resolver un complejo problema de optimización ni a implementar uno de los algoritmos cuánticos que aparecen habitualmente en los libros. Para empezar no nos hace falta nada de eso. Nos basta con un cúbit, una puerta cuántica y una medición.
Mi propósito para este artículo es desmitificar la complejidad de la computación cuántica y que veas con un ejemplo real implementado en Python, usando la librería Qiskit de IBM, cómo ejecutar algo genuinamente útil.
Si eres programador, probablemente estés acostumbrado al típico Hello, World! cuando pruebas un nuevo lenguaje o framework. Este ejemplo pretende cumplir aproximadamente la misma función con Qiskit: escribir unas pocas líneas, ejecutarlas y, sobre todo, comprender qué está ocurriendo detrás.
Lo interesante es que nuestro pequeño programa ya contiene algunos de los ingredientes fundamentales de la computación cuántica: estados, superposición, puertas, medición, circuitos y ejecución, de modo que verás cómo se pone todo en la misma cesta para construir algo útil. Vamos paso a paso.
Qiskit y un cúbit son suficientes
Qiskit es un SDK mantenido por IBM que permite construir y ejecutar circuitos cuánticos desde Python. En lugar de trabajar directamente con las particularidades físicas de un procesador cuántico, podemos describir un circuito mediante cúbits y puertas de una forma bastante parecida a como componemos otras abstracciones en software.
Nuestro generador de números aleatorios utilizará un único cúbit. Al crear el circuito, ese cúbit comienza en el estado computacional
(Ya estamos con las descripciones matemáticas, pero es que en computación cuántica, conviene que tengas conocimiento del algebra lineal necesaria y la notación de Dirac).
A continuación aplicaremos una puerta Hadamard, normalmente representada por . Esta operación transforma el estado anterior en
Un momento, ¿qué es la puerta de Hadamard?
La puerta Hadamard (H) es una de las operaciones más utilizadas en computación cuántica. Aplicada a un cúbit que está en el estado |0⟩, lo coloca en una superposición equilibrada de |0⟩ y |1⟩. Esto significa que, al medirlo, obtendremos 0 o 1 con la misma probabilidad, un 50 % para cada resultado. En nuestro pequeño programa, esta es la operación que nos permite obtener un bit aleatorio. Una analogía sencilla sería: no es lanzar una moneda y ocultar el resultado, sino preparar un estado cuántico que, al medirlo, produce uno de los dos resultados con igual probabilidad. Esta puerta configura un cúbit en estado de superposición.
La expresión anterior significa que las probabilidades asociadas a los dos posibles resultados de una medición en la base computacional son iguales:
Conviene detenerse aquí porque esta es precisamente la parte cuántica del programa. Antes de medir no debemos interpretar la expresión anterior como si el cúbit tuviese guardado un 0 o un 1 que simplemente desconocemos. El estado es una superposición de ambos estados de la base, y hasta medirlo, no tenemos ni idea de qué estado tiene realmente (misterios de la mecánica cuántica, literalmente). Cuando realizamos la medición obtenemos uno de los resultados posibles de acuerdo con las probabilidades determinadas por las amplitudes del estado.
Y con esto ya tenemos el mecanismo que necesitamos para producir un bit aleatorio.
El primer circuito
Tras instalar Qiskit en un entorno de Python, podemos definir nuestro circuito de este modo:
from qiskit import QuantumCircuit
circuit = QuantumCircuit(1, 1)
circuit.h(0)
circuit.measure(0, 0)
print(circuit.draw())
No hay mucho código, pero sí bastantes conceptos escondidos en esas líneas, y éstos son los que me interesa explicarte en este artículo.
QuantumCircuit(1, 1) crea un circuito con un cúbit y un bit clásico. Necesitamos este último porque el resultado de la medición debe almacenarse en información clásica que nuestro programa pueda leer después.
La llamada circuit.h(0) aplica la puerta Hadamard sobre el cúbit con índice cero (yo hubiese implementado este nombre de función como circuit.hadarmad(0), para facilitar las cosas a los neófitos).
Finalmente, circuit.measure(0, 0) mide ese cúbit y deposita el resultado en el bit clásico.
Recuerda: todos los circuitos de computación cuántica comienzan con una entrada de cúbits, que, tras su procesamiento y medirlos generan bits clásicos.
Eso es todo el circuito. Crear una aplicación cuántica no significa necesariamente escribir cientos de líneas de código. La dificultad aparece cuando queremos diseñar circuitos que transformen amplitudes e interferencias de una forma útil para resolver problemas concretos.
Antes del hardware real: un simulador
No necesitamos disponer de un computador cuántico para aprender Qiskit. Podemos ejecutar circuitos mediante simuladores clásicos.
Para este primer ejemplo podemos utilizar StatevectorSampler, incluido en Qiskit (que, por otra parte, incluye otro tipo de simuladores):
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler
circuit = QuantumCircuit(1, 1)
circuit.h(0)
circuit.measure(0, 0)
sampler = StatevectorSampler()
job = sampler.run([circuit], shots=100)
result = job.result()
counts = result[0].data.c.get_counts()
print(counts)
El parámetro shots=100 indica que queremos repetir la ejecución cien veces. Si todo va como esperamos, aparecerán aproximadamente tantos ceros como unos. No debemos esperar necesariamente 50 y 50: hablamos de un proceso probabilístico y una muestra finita puede producir, por ejemplo, 47 ceros y 53 unos.
Salvando las distancias, si tiras una moneda al aire cinco veces o un número pequeño de veces, seguramente obtengas resultados desequilibrados, pero si la lanzas un gran número de veces, el resultado irá tendiendo poco a poco al 50% para cara y cruz.
Hay aquí un matiz importante. Un simulador cuántico sigue siendo software ejecutándose en un computador clásico, en tu propio portátil o PC, vaya. Simula matemáticamente la evolución del circuito, lo que resulta extraordinariamente útil para aprender, desarrollar y comprobar nuestros programas, pero no convierte nuestro ordenador en una máquina cuántica.
Esta diferencia se vuelve especialmente importante cuando los circuitos crecen y en aplicaciones reales aumenta enormemente la complejidad del circuito a ejecutar así como la preparación de los datos a evaluar. Simular de forma general un sistema cuántico exige representar un espacio de estados cuyo tamaño aumenta exponencialmente con el número de cúbits (glups!). Un circuito pequeño es trivial para nuestro portátil; uno suficientemente grande deja de serlo.
De bits a números
Obtener un 0 o un 1 está bien para comprender el mecanismo, pero podemos avanzar un poco más. Si usamos varios cúbits en superposición y los medimos, obtendremos cadenas de bits que después podemos interpretar como enteros.
Por ejemplo, con ocho cúbits tenemos resultados comprendidos entre 00000000 y 11111111, es decir, entre 0 y 255:
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler
NUM_QUBITS = 8
circuit = QuantumCircuit(NUM_QUBITS, NUM_QUBITS)
circuit.h(range(NUM_QUBITS))
circuit.measure(range(NUM_QUBITS), range(NUM_QUBITS))
sampler = StatevectorSampler()
job = sampler.run([circuit], shots=1)
result = job.result()
counts = result[0].data.c.get_counts()
bit_string = next(iter(counts))
random_number = int(bit_string, 2)
print(bit_string)
print(random_number)
De nuevo, el código clásico que convierte la cadena binaria en un entero no tiene nada de cuántico. La parte cuántica termina en la medición. Esta separación es interesante porque muestra muy bien la naturaleza híbrida de buena parte del software cuántico actual: preparamos el experimento y procesamos sus resultados utilizando software clásico, mientras que delegamos determinadas operaciones al circuito cuántico.
En la práctica real, este es el flujo de trabajo habitual: se implementa la solución, se valida con simuladores y finalmente, se ejecuta en un computador cuántico real.
Ejecutarlo en un computador cuántico de IBM
Hasta ahora no hemos necesitado hardware cuántico real. Qiskit también permite enviar circuitos a sistemas de IBM Quantum mediante qiskit-ibm-runtime.
Necesitarás una cuenta de IBM Quantum y configurar las credenciales correspondientes. No debes introducir el token directamente en el código que después vas a publicar o subir a GitHub. La documentación de IBM debe ser la referencia para comprobar el procedimiento vigente de autenticación.
Un flujo básico para seleccionar un backend físico puede tener este aspecto:
from qiskit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2 as Sampler
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
circuit = QuantumCircuit(1, 1)
circuit.h(0)
circuit.measure(0, 0)
pass_manager = generate_preset_pass_manager(
target=backend.target,
optimization_level=1,
)
isa_circuit = pass_manager.run(circuit)
sampler = Sampler(mode=backend)
job = sampler.run([isa_circuit], shots=100)
result = job.result()[0]
print(result.data.c.get_counts())
Como vemos, tampoco parece más complicado este snippet de código si lo lees con atención.
Un momento, ¿cómo se traduce el circuito que implementamos a las instrucciones reales que ejecuta un computador cuántico? Aquí entra en juego el transpiler.
El circuito que nosotros escribimos es una descripción lógica. Un dispositivo concreto tiene restricciones sobre las operaciones que puede ejecutar y sobre cómo están conectados sus cúbits. Antes de enviarlo, Qiskit transforma nuestro circuito para adaptarlo al conjunto de instrucciones y a la topología del backend elegido.
En un procesador real los resultados tampoco tienen por qué presentar exactamente la distribución ideal. Además de la variación estadística propia de la medición, el hardware cuántico actual está sujeto a ruido y errores. Precisamente por eso conviene comenzar desarrollando y comprobando circuitos pequeños en simuladores y utilizar hardware real cuando queramos experimentar con su comportamiento físico.
¿Tenemos ya una aplicación cuántica?
Sí, aunque sea diminuta y de ejemplo.
Hemos partido de un cúbit en , aplicado una transformación unitaria mediante una puerta Hadamard, creado una superposición y realizado una medición. Después hemos convertido el resultado cuántico en información clásica que nuestro programa puede utilizar. Finalmente, hemos visto que el mismo circuito puede probarse localmente y prepararse para ejecutarse sobre un backend físico, real.
Todo muy básico, pero te aseguro que este es el esquema real de aplicación reales de casos de uso en computación básica.
Por último, no hemos demostrado ninguna ventaja cuántica, ni este pequeño programa pretende competir con los generadores de números aleatorios utilizados en sistemas reales. Un generador cuántico de números aleatorios destinado a usos serios, especialmente criptográficos, requiere consideraciones adicionales sobre el dispositivo, caracterización del ruido, sesgos, extracción de aleatoriedad y validación que quedan completamente fuera de este ejemplo didáctico (o al menos eso es lo que he leído por ahí :-).
Y precisamente ahí está para mí el valor de este Hello, World!: el circuito es tan sencillo que no distrae de los conceptos fundamentales. En unas pocas líneas aparecen ya buena parte de las piezas que volverás a encontrarte al programar circuitos más interesantes.
La siguiente vez que veas un circuito con muchos cúbits y puertas, recuerda este primero: preparar un estado, transformarlo, medirlo y utilizar el resultado. A partir de aquí aumenta la complejidad, claro, pero el vocabulario básico está descrito en esos breves pasos.