Algoritmo de Shor
Estimación de uso: Tres segundos en un procesador Eagle r3 (NOTA: Esto es solo una estimación. Su tiempo de ejecución podría variar.)
Resultados de aprendizaje
Tras completar este tutorial, los usuarios deberán comprender:
- El contexto matemático del algoritmo de Shor para la factorización de enteros
- Cómo ejecutar una instancia de ejemplo de este algoritmo en hardware
Requisitos previos
Sugerimos que los usuarios estén familiarizados con los siguientes temas antes de completar este tutorial:
- Fundamentos de algoritmos cuánticos.
- Estimación de fase y factorización. Cubrimos parte de este material en este tutorial.
Antecedentes
El algoritmo de Shor, desarrollado por Peter Shor en 1994, es un algoritmo cuántico revolucionario para factorizar enteros en tiempo polinomial. Su importancia radica en su capacidad para factorizar enteros grandes exponencialmente más rápido que cualquier algoritmo clásico conocido, amenazando la seguridad de los sistemas criptográficos ampliamente utilizados como RSA, que dependen de la dificultad de factorizar números grandes. Al resolver eficientemente este problema en una computadora cuántica suficientemente potente, el algoritmo de Shor podría revolucionar campos como la criptografía, la ciberseguridad y las matemáticas computacionales, subrayando el poder transformador de la computación cuántica.
Este tutorial se centra en demostrar el algoritmo de Shor factorizando 15 en una computadora cuántica.
Primero, definimos el problema de búsqueda de orden y construimos los circuitos correspondientes a partir del protocolo de estimación de fase cuántica. A continuación, ejecutamos los circuitos de búsqueda de orden en hardware real utilizando los circuitos de menor profundidad que podemos transpilar. La última sección completa el algoritmo de Shor conectando el problema de búsqueda de orden con la factorización de enteros.
Finalizamos el tutorial con una discusión sobre otras demostraciones del algoritmo de Shor en hardware real, centrándonos tanto en implementaciones genéricas como en aquellas diseñadas específicamente para factorizar enteros específicos como 15 y 21. Nota: Este tutorial se centra más en la implementación y demostración de los circuitos relacionados con el algoritmo de Shor. Para un recurso educativo en profundidad sobre el material, consulta el curso Fundamentals of quantum algorithms del Dr. John Watrous, y los artículos en la sección de Referencias.
Requisitos
Antes de comenzar este tutorial, asegúrate de tener instalado lo siguiente:
- Qiskit SDK v2.0 o posterior, con soporte de visualización
- Qiskit Runtime v0.40 o posterior (
pip install qiskit-ibm-runtime)
Configuración
# Added by doQumentation — required packages for this notebook
!pip install -q numpy pandas qiskit qiskit-ibm-runtime
import numpy as np
import pandas as pd
from fractions import Fraction
from math import floor, gcd, log
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit.library import QFT, UnitaryGate
from qiskit.transpiler import CouplingMap, generate_preset_pass_manager
from qiskit.visualization import plot_histogram
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2 as Sampler
Paso 1: Mapear entradas clásicas a un problema cuántico
El algoritmo de Shor para la factorización de enteros utiliza un problema intermediario conocido como el problema de búsqueda de orden. En esta sección, demostramos cómo resolver el problema de búsqueda de orden utilizando la estimación de fase cuántica.
Problema de estimación de fase
En el problema de estimación de fase, se nos proporciona un estado cuántico de qubits, junto con un circuito cuántico unitario que actúa sobre qubits. Se nos garantiza que es un vector propio de la matriz unitaria que describe la acción del circuito, y nuestro objetivo es calcular o aproximar el valor propio al cual corresponde . En otras palabras, el circuito debería producir una aproximación del número que satisface
El objetivo del circuito de estimación de fase es aproximar en bits. Matemáticamente hablando, deseamos encontrar tal que , donde . La siguiente imagen muestra el circuito cuántico que estima en bits realizando una medición sobre qubits.
En el circuito anterior, los qubits superiores se inicializan en el estado , y los qubits inferiores se inicializan en , el cual se garantiza que es un vector propio de . El primer ingrediente en el circuito de estimación de fase son las operaciones unitarias controladas que se encargan de realizar un retroceso de fase (phase kickback) en su qubit de control correspondiente. Estas unitarias controladas se exponencian de acuerdo con la posición del qubit de control, desde el bit menos significativo hasta el bit más significativo. Dado que es un vector propio de , el estado de los qubits inferiores no se ve afectado por esta operación, pero la información de fase del valor propio se propaga a los qubits superiores.
Resulta que después de la operación de retroceso de fase mediante unitarias controladas, todos los estados posibles de los qubits superiores son ortonormales entre sí para cada vector propio del operador unitario . Por lo tanto, estos estados son perfectamente distinguibles, y podemos rotar la base que forman de vuelta a la base computacional para realizar una medición. Un análisis matemático muestra que esta matriz de rotación corresponde a la transformada cuántica de Fourier (QFT) inversa en el espacio de Hilbert de dimensión . La intuición detrás de esto es que la estructura periódica de los operadores de exponenciación modular está codificada en el estado cuántico, y la QFT convierte esta periodicidad en picos medibles en el dominio de frecuencia.
Para una comprensión más profunda de por qué se emplea el circuito QFT en el algoritmo de Shor, remitimos al lector al curso Fundamentals of quantum algorithms. Ahora estamos listos para utilizar el circuito de estimación de fase para la búsqueda de orden.
Problema de búsqueda de orden
Para definir el problema de búsqueda de orden, comenzamos con algunos conceptos de teoría de números. Primero, para cualquier entero positivo dado , definimos el conjunto como Todas las operaciones aritméticas en se realizan módulo . En particular, todos los elementos que son coprimos con son especiales y constituyen como Para un elemento , el entero positivo más pequeño tal que se define como el orden de módulo . Como veremos más adelante, encontrar el orden de un nos permitirá factorizar . Para construir el circuito de búsqueda de orden a partir del circuito de estimación de fase, necesitamos dos consideraciones. Primero, necesitamos definir el operador unitario que nos permitirá encontrar el orden , y segundo, necesitamos definir un vector propio de para preparar el estado inicial del circuito de estimación de fase.
Para conectar el problema de búsqueda de orden con la estimación de fase, consideramos la operación definida sobre un sistema cuyos estados clásicos corresponden a , donde multiplicamos por un elemento fijo . En particular, definimos este operador de multiplicación tal que para cada . Observa que está implícito que estamos tomando el producto módulo dentro del ket en el lado derecho de la ecuación. Un análisis matemático muestra que es un operador unitario. Además, resulta que tiene pares de vectores propios y valores propios que nos permiten conectar el orden de con el problema de estimación de fase. Específicamente, para cualquier elección de , tenemos que es un vector propio de cuyo valor propio correspondiente es , donde Por observación, vemos que un par conveniente de vector propio/valor propio es el estado con . Por lo tanto, si pudiéramos encontrar el vector propio , podríamos estimar la fase con nuestro circuito cuántico y así obtener una estimación del orden . Sin embargo, no es fácil hacerlo, y necesitamos considerar una alternativa.
Consideremos qué resultado produciría el circuito si preparamos el estado computacional como estado inicial. Este no es un estado propio de , pero es la superposición uniforme de los estados propios que acabamos de describir. En otras palabras, se cumple la siguiente relación: La implicación de la ecuación anterior es que si establecemos el estado inicial como , obtendremos precisamente el mismo resultado de medición que si hubiéramos elegido uniformemente al azar y hubiéramos usado como vector propio en el circuito de estimación de fase. En otras palabras, una medición de los qubits superiores produce una aproximación al valor donde se elige uniformemente al azar. Esto nos permite aprender con un alto grado de confianza después de varias ejecuciones independientes, que era nuestro objetivo.
Operadores de exponenciación modular
Hasta ahora, vinculamos el problema de estimación de fase con el problema de búsqueda de orden definiendo y en nuestro circuito cuántico. Por lo tanto, el último ingrediente restante es encontrar una forma eficiente de definir las exponenciales modulares de como para . Para realizar este cálculo, encontramos que para cualquier potencia que elijamos, podemos crear un circuito para no iterando veces el circuito para , sino calculando y luego usando el circuito para . Dado que solo necesitamos las potencias que son a su vez potencias de 2, podemos hacer esto clásicamente de forma eficiente usando la elevación al cuadrado iterativa.
Paso 2: Optimizar el problema para la ejecución en hardware cuántico
Ejemplo específico con y
Podemos hacer una pausa aquí para discutir un ejemplo específico y construir el circuito de búsqueda de orden para . Observa que los posibles valores no triviales de para son . Para este ejemplo, elegimos . Construiremos el operador y los operadores de exponenciación modular . La acción de sobre los estados de la base computacional es la siguiente. Por observación, podemos ver que los estados de la base se reorganizan, por lo que tenemos una matriz de permutación. Podemos construir esta operación en cuatro qubits con compuertas de intercambio (swap). A continuación, construimos las operaciones y controlada.
def M2mod15():
"""
M2 (mod 15)
"""
b = 2
U = QuantumCircuit(4)
U.swap(2, 3)
U.swap(1, 2)
U.swap(0, 1)
U = U.to_gate()
U.name = f"M_{b}"
return U
# Get the M2 operator
M2 = M2mod15()
# Add it to a circuit and plot
circ = QuantumCircuit(4)
circ.compose(M2, inplace=True)
circ.decompose(reps=2).draw(output="mpl", fold=-1)
def controlled_M2mod15():
"""
Controlled M2 (mod 15)
"""
b = 2
U = QuantumCircuit(4)
U.swap(2, 3)
U.swap(1, 2)
U.swap(0, 1)
U = U.to_gate()
U.name = f"M_{b}"
c_U = U.control()
return c_U
# Get the controlled-M2 operator
controlled_M2 = controlled_M2mod15()
# Add it to a circuit and plot
circ = QuantumCircuit(5)
circ.compose(controlled_M2, inplace=True)
circ.decompose(reps=1).draw(output="mpl", fold=-1)
Las compuertas que actúan sobre más de dos qubits se descompondrán adicionalmente en compuertas de dos qubits.
circ.decompose(reps=2).draw(output="mpl", fold=-1)

Ahora necesitamos construir los operadores de exponenciación modular. Para obtener suficiente precisión en la estimación de fase, utilizaremos ocho qubits para la medición de estimación. Por lo tanto, necesitamos construir con para cada .
def a2kmodN(a, k, N):
"""Compute a^{2^k} (mod N) by repeated squaring"""
for _ in range(k):
a = int(np.mod(a**2, N))
return a
k_list = range(8)
b_list = [a2kmodN(2, k, 15) for k in k_list]
print(b_list)
[2, 4, 1, 1, 1, 1, 1, 1]
Como podemos ver en la lista de valores de , además de que construimos previamente, también necesitamos construir y . Observa que actúa trivialmente sobre los estados de la base computacional, por lo que es simplemente el operador identidad.
actúa sobre los estados de la base computacional de la siguiente manera.
Por lo tanto, esta permutación puede construirse con la siguiente operación de intercambio.
def M4mod15():
"""
M4 (mod 15)
"""
b = 4
U = QuantumCircuit(4)
U.swap(1, 3)
U.swap(0, 2)
U = U.to_gate()
U.name = f"M_{b}"
return U
# Get the M4 operator
M4 = M4mod15()
# Add it to a circuit and plot
circ = QuantumCircuit(4)
circ.compose(M4, inplace=True)
circ.decompose(reps=2).draw(output="mpl", fold=-1)
def controlled_M4mod15():
"""
Controlled M4 (mod 15)
"""
b = 4
U = QuantumCircuit(4)
U.swap(1, 3)
U.swap(0, 2)
U = U.to_gate()
U.name = f"M_{b}"
c_U = U.control()
return c_U
# Get the controlled-M4 operator
controlled_M4 = controlled_M4mod15()
# Add it to a circuit and plot
circ = QuantumCircuit(5)
circ.compose(controlled_M4, inplace=True)
circ.decompose(reps=1).draw(output="mpl", fold=-1)
Las compuertas que actúan sobre más de dos qubits se descompondrán adicionalmente en compuertas de dos qubits.
circ.decompose(reps=2).draw(output="mpl", fold=-1)

Vimos que los operadores para un dado son operaciones de permutación. Debido al tamaño relativamente pequeño del problema de permutación que tenemos aquí, dado que requiere solo cuatro qubits, pudimos sintetizar estas operaciones directamente con compuertas SWAP por inspección. En general, este podría no ser un enfoque escalable. En su lugar, podríamos necesitar construir la matriz de permutación explícitamente y usar la clase UnitaryGate de Qiskit y los métodos de transpilación para sintetizar esta matriz de permutación. Sin embargo, esto puede resultar en circuitos significativamente más profundos. A continuación se muestra un ejemplo.
def mod_mult_gate(b, N):
"""
Modular multiplication gate from permutation matrix.
"""
if gcd(b, N) > 1:
print(f"Error: gcd({b},{N}) > 1")
else:
n = floor(log(N - 1, 2)) + 1
U = np.full((2**n, 2**n), 0)
for x in range(N):
U[b * x % N][x] = 1
for x in range(N, 2**n):
U[x][x] = 1
G = UnitaryGate(U)
G.name = f"M_{b}"
return G
# Let's build M2 using the permutation matrix definition
M2_other = mod_mult_gate(2, 15)
# Add it to a circuit
circ = QuantumCircuit(4)
circ.compose(M2_other, inplace=True)
circ = circ.decompose()
# Transpile the circuit and get the depth
coupling_map = CouplingMap.from_line(4)
pm = generate_preset_pass_manager(coupling_map=coupling_map)
transpiled_circ = pm.run(circ)
print(f"qubits: {circ.num_qubits}")
print(
f"2q-depth: {transpiled_circ.depth(lambda x: x.operation.num_qubits==2)}"
)
print(f"2q-size: {transpiled_circ.size(lambda x: x.operation.num_qubits==2)}")
print(f"Operator counts: {transpiled_circ.count_ops()}")
transpiled_circ.decompose().draw(
output="mpl", fold=-1, style="clifford", idle_wires=False
)
qubits: 4
2q-depth: 94
2q-size: 96
Operator counts: OrderedDict({'cx': 45, 'swap': 32, 'u': 24, 'u1': 7, 'u3': 4, 'unitary': 3, 'circuit-335': 1, 'circuit-338': 1, 'circuit-341': 1, 'circuit-344': 1, 'circuit-347': 1, 'circuit-350': 1, 'circuit-353': 1, 'circuit-356': 1, 'circuit-359': 1, 'circuit-362': 1, 'circuit-365': 1, 'circuit-368': 1, 'circuit-371': 1, 'circuit-374': 1, 'circuit-377': 1, 'circuit-380': 1})

Comparemos estos conteos con la profundidad del circuito compilado de nuestra implementación manual de la compuerta .
# Get the M2 operator from our manual construction
M2 = M2mod15()
# Add it to a circuit
circ = QuantumCircuit(4)
circ.compose(M2, inplace=True)
circ = circ.decompose(reps=3)
# Transpile the circuit and get the depth
coupling_map = CouplingMap.from_line(4)
pm = generate_preset_pass_manager(coupling_map=coupling_map)
transpiled_circ = pm.run(circ)
print(f"qubits: {circ.num_qubits}")
print(
f"2q-depth: {transpiled_circ.depth(lambda x: x.operation.num_qubits==2)}"
)
print(f"2q-size: {transpiled_circ.size(lambda x: x.operation.num_qubits==2)}")
print(f"Operator counts: {transpiled_circ.count_ops()}")
transpiled_circ.draw(
output="mpl", fold=-1, style="clifford", idle_wires=False
)
qubits: 4
2q-depth: 9
2q-size: 9
Operator counts: OrderedDict({'cx': 9})
Como podemos ver, el enfoque de la matriz de permutación resultó en un circuito significativamente más profundo incluso para una sola compuerta en comparación con nuestra implementación manual. Por lo tanto, continuaremos con nuestra implementación anterior de las operaciones . Ahora estamos listos para construir el circuito completo de búsqueda de orden utilizando nuestros operadores de exponenciación modular controlada previamente definidos. En el siguiente código, también importamos el circuito QFT de la biblioteca de circuitos de Qiskit, que utiliza compuertas Hadamard en cada qubit, una serie de compuertas controladas U1 (o Z, dependiendo de la fase) y una capa de compuertas de intercambio.
# Order finding problem for N = 15 with a = 2
N = 15
a = 2
# Number of qubits
num_target = floor(log(N - 1, 2)) + 1 # for modular exponentiation operators
num_control = 2 * num_target # for enough precision of estimation
# List of M_b operators in order
k_list = range(num_control)
b_list = [a2kmodN(2, k, 15) for k in k_list]
# Initialize the circuit
control = QuantumRegister(num_control, name="C")
target = QuantumRegister(num_target, name="T")
output = ClassicalRegister(num_control, name="out")
circuit = QuantumCircuit(control, target, output)
# Initialize the target register to the state |1>
circuit.x(num_control)
# Add the Hadamard gates and controlled versions of the
# multiplication gates
for k, qubit in enumerate(control):
circuit.h(k)
b = b_list[k]
if b == 2:
circuit.compose(
M2mod15().control(), qubits=[qubit] + list(target), inplace=True
)
elif b == 4:
circuit.compose(
M4mod15().control(), qubits=[qubit] + list(target), inplace=True
)
else:
continue # M1 is the identity operator
# Apply the inverse QFT to the control register
circuit.compose(QFT(num_control, inverse=True), qubits=control, inplace=True)
# Measure the control register
circuit.measure(control, output)
circuit.draw("mpl", fold=-1)
Observa que omitimos las operaciones de exponenciación modular controlada de los qubits de control restantes porque es el operador identidad.
Ten en cuenta que más adelante en este tutorial, ejecutaremos este circuito en el backend ibm_marrakesh. Para ello, transpilamos el circuito de acuerdo con este backend específico e informamos la profundidad del circuito y los conteos de compuertas.
service = QiskitRuntimeService()
backend = service.backend("ibm_marrakesh")
pm = generate_preset_pass_manager(optimization_level=2, backend=backend)
transpiled_circuit = pm.run(circuit)
print(
f"2q-depth: {transpiled_circuit.depth(lambda x: x.operation.num_qubits==2)}"
)
print(
f"2q-size: {transpiled_circuit.size(lambda x: x.operation.num_qubits==2)}"
)
print(f"Operator counts: {transpiled_circuit.count_ops()}")
transpiled_circuit.draw(
output="mpl", fold=-1, style="clifford", idle_wires=False
)
2q-depth: 187
2q-size: 260
Operator counts: OrderedDict({'sx': 521, 'rz': 354, 'cz': 260, 'measure': 8, 'x': 4})
