Saltar al contenido principal

Algoritmo de Grover

Estimación de uso: menos de un minuto en un procesador Eagle r3 (NOTA: Esta es solo una estimación. Su tiempo de ejecución puede variar.)

Resultados de aprendizaje

Tras completar este tutorial, podrás comprender lo siguiente:

  • Cómo construir oráculos de Grover que marquen uno o más estados de base computacional
  • Cómo usar la función grover_operator() de la biblioteca de circuitos de Qiskit
  • Cómo determinar el número óptimo de iteraciones de Grover para un problema dado
  • Cómo ejecutar el algoritmo de Grover usando la primitiva Sampler de Qiskit Runtime

Requisitos previos

Se recomienda que te familiarices con estos temas:

Antecedentes

La amplificación de amplitud es un algoritmo cuántico de propósito general, o subrutina, que puede usarse para obtener una aceleración cuadrática sobre un puñado de algoritmos clásicos. El algoritmo de Grover fue el primero en demostrar esta aceleración en problemas de búsqueda no estructurada. Formular un problema de búsqueda de Grover requiere una función oráculo que marque uno o más estados de base computacional como los estados que nos interesa encontrar, y un circuito de amplificación que aumenta la amplitud de los estados marcados, suprimiendo consecuentemente los estados restantes.

Aquí, demostramos cómo construir oráculos de Grover y usar grover_operator() de la biblioteca de circuitos de Qiskit para configurar fácilmente una instancia de búsqueda de Grover. La primitiva Sampler de runtime permite una ejecución fluida de circuitos de Grover.

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.22 o posterior (pip install qiskit-ibm-runtime)

Configuración

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib qiskit qiskit-ibm-runtime
# Built-in modules
import math

# Imports from Qiskit
from qiskit import QuantumCircuit
from qiskit.circuit.library import grover_operator, MCMTGate, ZGate
from qiskit.visualization import plot_distribution
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

# Imports from Qiskit Runtime
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2 as Sampler

def grover_oracle(marked_states):
"""Build a Grover oracle for multiple marked states

Here we assume all input marked states have the same number of bits

Parameters:
marked_states (str or list): Marked states of oracle

Returns:
QuantumCircuit: Quantum circuit representing Grover oracle
"""
if not isinstance(marked_states, list):
marked_states = [marked_states]
# Compute the number of qubits in circuit
num_qubits = len(marked_states[0])

qc = QuantumCircuit(num_qubits)
# Mark each target state in the input list
for target in marked_states:
# Flip target bit-string to match Qiskit bit-ordering
rev_target = target[::-1]
# Find the indices of all the '0' elements in bit-string
zero_inds = [
ind
for ind in range(num_qubits)
if rev_target.startswith("0", ind)
]
# Add a multi-controlled Z-gate with pre- and post-applied X-gates (open-controls)
# where the target bit-string has a '0' entry
if zero_inds:
qc.x(zero_inds)
qc.compose(MCMTGate(ZGate(), num_qubits - 1, 1), inplace=True)
if zero_inds:
qc.x(zero_inds)
return qc

Ejemplo de simulación a pequeña escala

En esta sección, recorremos cada paso del algoritmo de Grover a pequeña escala usando un simulador local, antes de ejecutar el mismo problema en hardware cuántico real.

Paso 1: Mapear entradas clásicas a un problema cuántico

El algoritmo de Grover requiere un oráculo que especifica uno o más estados de base computacional marcados, donde "marcado" significa un estado con una fase de -1. Un gate Z controlado, o su generalización multi-controlada sobre NN qubits, marca el estado 2N12^{N}-1 (cadena de bits '1'*NN). Marcar estados de base con uno o más '0' en la representación binaria requiere aplicar gates X en los qubits correspondientes antes y después del gate Z controlado, lo que equivale a tener un control abierto en ese qubit. En el siguiente código, definimos un oráculo que marca uno o más estados de base de entrada definidos a través de su representación de cadena de bits. El gate MCMT se usa para implementar el gate Z multi-controlado.

Instancia específica de Grover

Ahora que tenemos la función oráculo, podemos definir una instancia específica de búsqueda de Grover. En este ejemplo marcaremos dos estados computacionales de los ocho disponibles en un espacio computacional de tres qubits:

marked_states = ["011", "100"]

oracle = grover_oracle(marked_states)
oracle.draw(output="mpl", style="iqp")

Output of the previous code cell

Operador de Grover

El grover_operator() integrado de Qiskit toma un circuito oráculo y devuelve un circuito que está compuesto por el circuito oráculo mismo y un circuito que amplifica los estados marcados por el oráculo. Aquí, usamos el método decompose() del circuito para ver los gates dentro del operador:

grover_op = grover_operator(oracle)
grover_op.decompose().draw(output="mpl", style="iqp")

Output of the previous code cell

Las aplicaciones repetidas de este circuito grover_op amplifican los estados marcados, haciéndolos las cadenas de bits más probables en la distribución de salida del circuito. Hay un número óptimo de tales aplicaciones que está determinado por la relación de estados marcados al número total de estados computacionales posibles:

optimal_num_iterations = math.floor(
math.pi
/ (4 * math.asin(math.sqrt(len(marked_states) / 2**grover_op.num_qubits)))
)

Circuito completo de Grover

Un experimento completo de Grover comienza con un gate Hadamard en cada qubit; creando una superposición uniforme de todos los estados de base computacional, seguido del operador de Grover (grover_op) repetido el número óptimo de veces. Aquí hacemos uso del método QuantumCircuit.power(INT) para aplicar repetidamente el operador de Grover.

qc = QuantumCircuit(grover_op.num_qubits)
# Create even superposition of all basis states
qc.h(range(grover_op.num_qubits))
# Apply Grover operator the optimal number of times
qc.compose(grover_op.power(optimal_num_iterations), inplace=True)
# Measure all qubits
qc.measure_all()
qc.draw(output="mpl", style="iqp")

Output of the previous code cell

Paso 2: Optimizar el problema para la ejecución en hardware cuántico

Para la simulación a pequeña escala, transpilamos el circuito sin apuntar a hardware específico.

pm = generate_preset_pass_manager(optimization_level=3)
circuit_isa = pm.run(qc)
circuit_isa.draw(output="mpl", idle_wires=False, style="iqp")

Output of the previous code cell

Paso 3: Ejecutar usando primitivas de Qiskit

La amplificación de amplitud es un problema de muestreo adecuado para la ejecución con la primitiva SamplerV2. Aquí usamos StatevectorSampler de qiskit.primitives para la simulación local.

from qiskit.primitives import StatevectorSampler

sampler = StatevectorSampler()
result = sampler.run([circuit_isa], shots=10_000).result()
dist = result[0].data.meas.get_counts()

Paso 4: Post-procesar y devolver el resultado en el formato clásico deseado

plot_distribution(dist)

Output of the previous code cell

Ejemplo de hardware

Pasos 1-4

El algoritmo de Grover es fundamentalmente un algoritmo tolerante a fallos — los gates Z multi-controlados en el núcleo del oráculo y el operador de difusión llevan a profundidades de gates de dos qubits que crecen muy rápidamente con el número de qubits (como mostraremos en la siguiente sección). Esto significa que el algoritmo no escala bien en el hardware ruidoso actual. Por esta razón, demostramos la ejecución en hardware a la misma pequeña escala que el ejemplo del simulador anterior, en lugar de intentar un tamaño de problema mayor.

# -------------------------Step 1-------------------------
marked_states = ["011", "100"]

oracle = grover_oracle(marked_states)
grover_op = grover_operator(oracle)

optimal_num_iterations = math.floor(
math.pi
/ (4 * math.asin(math.sqrt(len(marked_states) / 2**grover_op.num_qubits)))
)

qc = QuantumCircuit(grover_op.num_qubits)
qc.h(range(grover_op.num_qubits))
qc.compose(grover_op.power(optimal_num_iterations), inplace=True)
qc.measure_all()

# -------------------------Step 2-------------------------
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)

target = backend.target
pm = generate_preset_pass_manager(target=target, optimization_level=3)
circuit_isa = pm.run(qc)

# -------------------------Step 3-------------------------
sampler = Sampler(mode=backend)
sampler.options.default_shots = 10_000
sampler.options.environment.job_tags = ["TUT-GA"]
result = sampler.run([circuit_isa]).result()
dist = result[0].data.meas.get_counts()

# -------------------------Step 4-------------------------
plot_distribution(dist)

Output of the previous code cell

Discusión: Escalado de la profundidad de gates de dos qubits

Una razón clave por la que el algoritmo de Grover se considera un algoritmo tolerante a fallos es el rápido crecimiento de la profundidad de gates de dos qubits del circuito a medida que aumenta el número de qubits. El gate Z multi-controlado en el núcleo tanto del oráculo como del operador de difusión se descompone en un número de gates de dos qubits que crece exponencialmente con el número de qubits de control. Combinado con el hecho de que el número óptimo de iteraciones de Grover crece como O(2n)O(\sqrt{2^n}), la profundidad total de dos qubits se vuelve rápidamente impráctica para el hardware ruidoso.

A continuación, construimos circuitos de Grover para conteos crecientes de qubits, los transpilamos y representamos la profundidad resultante de gates de dos qubits para ilustrar este escalado.

import matplotlib.pyplot as plt

num_qubits_list = list(range(3, 10))
two_q_depths = []
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
for n in num_qubits_list:
# Mark a single state for simplicity
marked = ["1" * n]
oracle_n = grover_oracle(marked)
grover_op_n = grover_operator(oracle_n)

# Optimal number of iterations
num_iters = math.floor(
math.pi / (4 * math.asin(math.sqrt(len(marked) / 2**n)))
)

# Build the full Grover circuit
qc_n = QuantumCircuit(n)
qc_n.h(range(n))
qc_n.compose(grover_op_n.power(num_iters), inplace=True)
qc_n.measure_all()

# Transpile to a basis gate set and count 2Q depth
pm_n = generate_preset_pass_manager(backend=backend, optimization_level=3)
qc_transpiled = pm_n.run(qc_n)

# Compute depth restricted to 2-qubit operations
depth_2q = qc_transpiled.depth(lambda x: x.operation.num_qubits == 2)

two_q_depths.append(depth_2q)
print(f"n={n}: optimal_iters={num_iters}, 2Q depth={depth_2q}")

# Plot
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(
num_qubits_list,
two_q_depths,
"o-",
linewidth=2,
markersize=8,
color="#6929C4",
)
ax.set_xlabel("Number of qubits", fontsize=13)
ax.set_ylabel("Two-qubit gate depth", fontsize=13)
ax.set_title("Grover's algorithm: 2Q depth scaling", fontsize=14)
ax.set_yscale("log")
ax.grid(True, alpha=0.3)
ax.set_xticks(num_qubits_list)
plt.tight_layout()
plt.show()
n=3: optimal_iters=2, 2Q depth=39
n=4: optimal_iters=3, 2Q depth=111
n=5: optimal_iters=4, 2Q depth=466
n=6: optimal_iters=6, 2Q depth=1646
n=7: optimal_iters=8, 2Q depth=3550
n=8: optimal_iters=12, 2Q depth=7989
n=9: optimal_iters=17, 2Q depth=14824

Output of the previous code cell

Como muestra la gráfica, la profundidad de gates de dos qubits crece extremadamente rápido con el número de qubits — aproximadamente de forma exponencial. Esto hace que el algoritmo de Grover sea impráctical en el hardware cuántico ruidoso actual más allá de tamaños de problema muy pequeños. El algoritmo sigue siendo un objetivo importante para futuras computadoras cuánticas tolerantes a fallos, donde la corrección de errores permitirá ejecutar circuitos profundos de manera fiable.

Próximos pasos

Recomendaciones

Si encontraste este trabajo interesante, puede que te interese el siguiente material: