Przejdź do głównej treści

Algorytm Grovera

Szacowany czas obliczeń: poniżej jednej minuty na procesorze Eagle r3 (UWAGA: Jest to tylko szacunek. Rzeczywisty czas może się różnić.)

Cele nauki

Po ukończeniu tego samouczka możesz spodziewać się zrozumienia następujących informacji:

  • Jak konstruować wyrocznie Grovera oznaczające jeden lub więcej stanów bazy obliczeniowej
  • Jak używać funkcji grover_operator() z biblioteki obwodów Qiskit
  • Jak wyznaczać optymalną liczbę iteracji Grovera dla danego problemu
  • Jak wykonywać algorytm Grovera przy użyciu prymitywu Sampler środowiska Qiskit Runtime

Wymagania wstępne

Zaleca się zapoznanie z poniższymi zagadnieniami:

Tło

Wzmocnienie amplitudy to ogólny algorytm kwantowy (lub podprogram), który można wykorzystać do uzyskania kwadratowego przyspieszenia względem wielu klasycznych algorytmów. Algorytm Grovera był pierwszym, który zademonstrował to przyspieszenie w przypadku przeszukiwania nieuporządkowanych danych. Sformułowanie problemu wyszukiwania Grovera wymaga funkcji wyroczni, która oznacza jeden lub więcej stanów bazy obliczeniowej jako stany, które chcemy znaleźć, oraz obwodu wzmacniającego, który zwiększa amplitudę oznaczonych stanów, a tym samym tłumi pozostałe.

Tutaj pokazujemy, jak konstruować wyrocznie Grovera i używać funkcji grover_operator() z biblioteki obwodów Qiskit, aby w prosty sposób skonfigurować instancję wyszukiwania Grovera. Primityw Sampler środowiska uruchomieniowego umożliwia bezproblemowe wykonywanie obwodów Grovera.

Wymagania

Przed rozpoczęciem tego samouczka upewnij się, że masz zainstalowane następujące elementy:

  • Qiskit SDK v2.0 lub nowszy, z obsługą wizualizacji
  • Qiskit Runtime v0.22 lub nowszy (pip install qiskit-ibm-runtime)

Konfiguracja

# 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

Przykład na małą skalę z symulatorem

W tej sekcji przechodzimy przez każdy krok algorytmu Grovera na małą skalę przy użyciu lokalnego symulatora, zanim uruchomimy ten sam problem na prawdziwym sprzęcie kwantowym.

Krok 1: Odwzorowanie klasycznych danych wejściowych na problem kwantowy

Algorytm Grovera wymaga wyroczni, która określa jeden lub więcej oznaczonych stanów bazy obliczeniowej, gdzie „oznaczony" oznacza stan z fazą -1. Gate kontrolowana-Z lub jej wielokrotnie kontrolowane uogólnienie na NN Qubitach oznacza stan 2N12^{N}-1 ('1'*NN bitów). Oznaczenie stanów bazy z co najmniej jednym '0' w reprezentacji binarnej wymaga zastosowania bramek X na odpowiednich Qubitach przed i po bramce controlled-Z — co jest równoważne posiadaniu otwartego sterowania na tym Qubicie. W poniższym kodzie definiujemy wyrocznię, która oznacza jeden lub więcej wejściowych stanów bazy, zdefiniowanych przez reprezentację bitową. Gate MCMT służy do implementacji wielokrotnie kontrolowanej bramki Z.

Konkretna instancja wyszukiwania Grovera

Teraz, gdy mamy funkcję wyroczni, możemy zdefiniować konkretną instancję wyszukiwania Grovera. W tym przykładzie oznaczymy dwa stany obliczeniowe spośród ośmiu dostępnych w trójQubitowej przestrzeni obliczeniowej:

marked_states = ["011", "100"]

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

Output of the previous code cell

Operator Grovera

Wbudowana funkcja Qiskit grover_operator() przyjmuje obwód wyroczni i zwraca obwód złożony z samego obwodu wyroczni oraz obwodu wzmacniającego stany oznaczone przez wyrocznię. Tutaj używamy metody decompose() na obwodzie, aby zobaczyć bramki wewnątrz operatora:

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

Output of the previous code cell

Wielokrotne zastosowania obwodu grover_op wzmacniają oznaczone stany, czyniąc je najbardziej prawdopodobnymi ciągami bitów w wynikowym rozkładzie z obwodu. Istnieje optymalna liczba takich zastosowań, wyznaczana przez stosunek liczby oznaczonych stanów do całkowitej liczby możliwych stanów obliczeniowych:

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

Pełny Circuit Grovera

Pełny eksperyment Grovera rozpoczyna się od bramki Hadamarda na każdym Qubicie, tworząc równą superpozycję wszystkich stanów bazy obliczeniowej, a następnie operator Grovera (grover_op) jest stosowany optymalną liczbę razy. Tutaj korzystamy z metody QuantumCircuit.power(INT), aby wielokrotnie zastosować operator Grovera.

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

Krok 2: Optymalizacja problemu pod kątem wykonania na sprzęcie kwantowym

Dla symulacji na małą skalę transpilujemy obwód bez wskazywania konkretnego sprzętu.

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

Krok 3: Wykonanie przy użyciu prymitywów Qiskit

Wzmocnienie amplitudy to problem próbkowania, który nadaje się do wykonania przy użyciu prymitywu SamplerV2. Tutaj używamy StatevectorSampler z qiskit.primitives do lokalnej symulacji.

from qiskit.primitives import StatevectorSampler

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

Krok 4: Przetwarzanie końcowe i zwrócenie wyniku w pożądanym formacie klasycznym

plot_distribution(dist)

Output of the previous code cell

Przykład na sprzęcie

Kroki 1–4

Algorytm Grovera jest z założenia algorytmem tolerującym błędy — wielokrotnie kontrolowane bramki Z stanowiące serce wyroczni i operatora dyfuzji prowadzą do głębokości obwodów dwu-Qubitowych, która rośnie bardzo szybko wraz z liczbą Qubitów (co pokażemy w następnej sekcji). Oznacza to, że algorytm nie skaluje się dobrze na obecnym zaszumionym sprzęcie. Z tego powodu demonstrujemy wykonanie na sprzęcie na tej samej małej skali co przykład z symulatorem, zamiast próbować rozwiązać większy problem.

# -------------------------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

Omówienie: skalowanie głębokości bramek dwu-Qubitowych

Kluczowym powodem, dla którego algorytm Grovera jest uważany za algorytm tolerujący błędy, jest szybki wzrost głębokości obwodów dwu-Qubitowych wraz ze wzrostem liczby Qubitów. Wielokrotnie kontrolowana bramka Z stanowiąca rdzeń zarówno wyroczni, jak i operatora dyfuzji rozkłada się na liczbę bramek dwu-Qubitowych rosnącą wykładniczo wraz z liczbą Qubitów sterujących. W połączeniu z faktem, że optymalna liczba iteracji Grovera sama w sobie rośnie jak O(2n)O(\sqrt{2^n}), całkowita głębokość dwu-Qubitowa szybko staje się niepraktyczna dla zaszumionego sprzętu.

Poniżej konstruujemy obwody Grovera dla rosnącej liczby Qubitów, transpilujemy je i rysujemy wynikową głębokość bramek dwu-Qubitowych, aby zilustrować to skalowanie.

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

Jak pokazuje wykres, głębokość bramek dwu-Qubitowych rośnie niezwykle szybko wraz z liczbą Qubitów — mniej więcej wykładniczo. Sprawia to, że algorytm Grovera jest niepraktyczny na obecnym zaszumionym sprzęcie kwantowym dla jakichkolwiek rozmiarów problemów poza bardzo małymi. Algorytm pozostaje ważnym celem dla przyszłych kwantowych komputerów tolerujących błędy, gdzie korekcja błędów umożliwi niezawodne wykonywanie głębokich obwodów.

Następne kroki

Zalecenia

Jeśli ta praca wydała ci się interesująca, możesz zainteresować się następującymi materiałami: