Przejdź do głównej treści

Redukcja głębokości Circuit z dodatkiem AQC-Tensor do Qiskit

W tym notebooku przejdziemy przez kolejne kroki wzorca Qiskit, korzystając z przybliżonej kompilacji kwantowej z sieciami tensorowymi (AQC-Tensor), aby osiągnąć mniejszą głębokość Circuit niż byłoby to zwykle konieczne przy ewolucji Trottera.

Oto kroki, które wykonamy:

  • Krok 1: Odwzorowanie na problem kwantowy
    • Inicjalizacja Hamiltonianu naszego problemu i obserwowalnych
    • Wygenerowanie docelowego stanu sieci tensorowej dla początkowej części Circuit
    • Wygenerowanie Circuit o małej głębokości, który aproksymuje kompresowaną część
    • Wygenerowanie ogólnego ansatzu na podstawie tego Circuit
    • Optymalizacja parametrów w celu maksymalnego zbliżenia ansatzu do celu
    • Dodanie kolejnych kroków Trottera do zoptymalizowanego ansatzu
  • Krok 2: Optymalizacja pod docelowy sprzęt
    • Transpilacja Circuit dla sprzętu
  • Krok 3: Wykonanie eksperymentów
    • Użycie fałszywego Backend dla uproszczenia
  • Krok 4: Rekonstrukcja wyników
    • Nie dotyczy; zamiast tego po prostu wyświetlamy zmierzoną obserwowalną

Krok 1: Odwzorowanie na Circuit kwantowy i operator

Konfiguracja modelowego Hamiltonianu i obserwowalnej

W tym notebooku używamy modelu Isinga na okręgu złożonym z 10 węzłów:

H^Ising=i=110Ji,(i+1)ZiZ(i+1)+hiXi,\hat{\mathcal{H}}_{\text{Ising}} = \sum_{i=1}^{10} J_{i,(i+1)} Z_i Z_{(i+1)} + h_i X_i \, ,

gdzie periodyczne warunki brzegowe oznaczają, że dla i=10i=10 otrzymujemy i+1=111i+1=11\rightarrow1, JJ to stała sprzężenia między dwoma węzłami, a hh to zewnętrzne pole magnetyczne.

# Added by doQumentation — required packages for this notebook
!pip install -q qiskit qiskit-addon-aqc-tensor qiskit-addon-utils qiskit-ibm-runtime quimb scipy
from qiskit.transpiler import CouplingMap
from qiskit_addon_utils.problem_generators import generate_xyz_hamiltonian

# Generate some coupling map to use for this example
coupling_map = CouplingMap.from_heavy_hex(3, bidirectional=False)

# Choose a 10-qubit circle on this coupling map
reduced_coupling_map = coupling_map.reduce([0, 13, 1, 14, 10, 16, 4, 15, 3, 9])

# Get a qubit operator describing the Ising field model
hamiltonian = generate_xyz_hamiltonian(
reduced_coupling_map,
coupling_constants=(0.0, 0.0, 1.0),
ext_magnetic_field=(0.4, 0.0, 0.0),
)

Obserwowalną, którą będziemy mierzyć, jest całkowita magnetyzacja.

from qiskit.quantum_info import SparsePauliOp

L = reduced_coupling_map.size()
observable = SparsePauliOp.from_sparse_list([("Z", [i], 1 / L / 2) for i in range(L)], num_qubits=L)

Określenie, jak dużą część ewolucji czasowej symulować klasycznie

Naszym ogólnym celem jest symulacja ewolucji czasowej powyższego modelu Hamiltonianu. Robimy to za pomocą ewolucji Trottera, którą dzielimy na dwie części:

  1. Część początkowa, którą można symulować za pomocą stanów iloczynów macierzowych (MPS). Tę część „skompilujemy" przy użyciu AQC, zgodnie z https://arxiv.org/abs/2301.08609.
  2. Kolejna część Circuit, która zostanie wykonana na sprzęcie. Planujemy użyć AQC-Tensor do kompresji naszego Circuit ewolucji czasowej do czasu t=4t=4, a następnie ewoluować zwykłymi krokami Trottera do t=5t=5.

Generowanie Circuit przed i po podziale

Skoro wybraliśmy podział w t=4t=4, wygenerujemy dwa Circuit:

  1. „Docelowy" Circuit dla części AQC ewolucji, od ti=0t_i=0 do tf=4t_f=4. Ponieważ jest on symulowany przez symulator sieci tensorowych, liczba warstw wpływa na czas wykonania jedynie przez stały współczynnik, więc możemy spokojnie użyć dużej liczby warstw, aby zminimalizować błąd Trottera.
from qiskit.synthesis import SuzukiTrotter
from qiskit_addon_utils.problem_generators import generate_time_evolution_circuit

aqc_evolution_time = 4.0
aqc_target_num_trotter_steps = 45

aqc_target_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_target_num_trotter_steps),
time=aqc_evolution_time,
)
  1. Kolejny Circuit ewolucji, który ewoluuje od ti=4t_i=4 do tf=5t_f=5. Ponieważ będzie on uruchamiany na sprzęcie kwantowym, pożądane jest użycie jak najmniejszej liczby warstw Trottera.
subsequent_evolution_time = 1.0
subsequent_num_trotter_steps = 5

subsequent_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=subsequent_num_trotter_steps),
time=subsequent_evolution_time,
)

Na potrzeby późniejszego porównania wygenerujmy również trzeci Circuit: taki, który ewoluuje przez aqc_evolution_time, ale ma ten sam czas ewolucji na krok Trottera co kolejny Circuit. To Circuit, z którym pracowalibyśmy, gdybyśmy nie użyli dużej liczby kroków Trottera dla docelowego Circuit. Będziemy odnosić się do niego jako do Circuit porównawczego.

aqc_comparison_num_trotter_steps = int(
subsequent_num_trotter_steps / subsequent_evolution_time * aqc_evolution_time
)
aqc_comparison_num_trotter_steps
20
comparison_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_comparison_num_trotter_steps),
time=aqc_evolution_time,
)

Generowanie ansatzu i parametrów początkowych z Circuit Trottera z mniejszą liczbą kroków

Najpierw konstruujemy „dobry" Circuit, który ma ten sam czas ewolucji co docelowy Circuit, ale z mniejszą liczbą kroków Trottera (a co za tym idzie, mniejszą liczbą warstw).

Następnie przekazujemy ten „dobry" Circuit do funkcji generate_ansatz_from_circuit z AQC-Tensor. Funkcja ta analizuje łączność dwuQubitową Circuit i zwraca dwie rzeczy:

  1. ogólny, sparametryzowany Circuit ansatzu o tej samej łączności dwuQubitowej co Circuit wejściowy; oraz
  2. parametry, które po podstawieniu do ansatzu dają wejściowy (dobry) Circuit.

Wkrótce weźmiemy te parametry i będziemy je iteracyjnie dostosowywać, aby maksymalnie zbliżyć Circuit ansatzu do docelowego MPS.

from qiskit_addon_aqc_tensor import generate_ansatz_from_circuit

aqc_ansatz_num_trotter_steps = 5

aqc_good_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_ansatz_num_trotter_steps),
time=aqc_evolution_time,
)

aqc_ansatz, aqc_initial_parameters = generate_ansatz_from_circuit(
aqc_good_circuit, qubits_initially_zero=True
)
aqc_ansatz.draw("mpl", fold=-1)

Quantum circuit diagram

print(f"Comparison circuit: depth {comparison_circuit.depth()}")
print(f"Target circuit: depth {aqc_target_circuit.depth()}")
print(f"Ansatz circuit: depth {aqc_ansatz.depth()}, with {len(aqc_initial_parameters)} parameters")
Comparison circuit: depth 120
Target circuit: depth 270
Ansatz circuit: depth 23, with 515 parameters

Wybór ustawień symulacji sieci tensorowej

Tutaj używamy symulatora sieci tensorowej opartego na quimb. W tym przykładzie korzystamy z symulatora stanów iloczynów macierzowych (MPS) biblioteki quimb oraz JAX do automatycznego różniczkowania. Więcej informacji o używaniu symulatora quimb znajdziesz w dokumentacji API.

from functools import partial

import quimb.tensor

from qiskit_addon_aqc_tensor.simulation.quimb import QuimbSimulator

simulator_settings = QuimbSimulator(
partial(quimb.tensor.CircuitMPS, max_bond=100, cutoff=1e-8),
autodiff_backend="jax",
)

Konstrukcja reprezentacji stanu docelowego AQC w postaci iloczynu macierzowego

Następnie budujemy reprezentację iloczynu macierzowego stanu, który ma być aproksymowany przez AQC.

from qiskit_addon_aqc_tensor.simulation import tensornetwork_from_circuit

aqc_target_mps = tensornetwork_from_circuit(aqc_target_circuit, simulator_settings)

Zauważ, że ponieważ wybraliśmy dużą liczbę kroków Trottera dla stanu docelowego, ma on faktycznie mniejszy błąd Trottera niż Circuit porównawczy. Możemy obliczyć wierność (ψ1ψ22| \langle \psi_1 | \psi_2 \rangle |^2) stanu przygotowanego przez Circuit porównawczy względem stanu docelowego:

from qiskit_addon_aqc_tensor.simulation import compute_overlap

comparison_mps = tensornetwork_from_circuit(comparison_circuit, simulator_settings)
comparison_fidelity = abs(compute_overlap(comparison_mps, aqc_target_mps)) ** 2
comparison_fidelity
0.9996761790297157

Optymalizacja parametrów ansatzu za pomocą obliczeń MPS

Tutaj minimalizujemy najprostszą możliwą funkcję kosztu, MaximizeStateFidelity, korzystając z optymalizatora L-BFGS z biblioteki scipy.

Wybieramy punkt zatrzymania dla wierności tak, aby była ona wyższa od tej, którą osiągnąłby Circuit porównawczy bez użycia AQC. Po jego osiągnięciu skompresowany Circuit ma mniejszy błąd Trottera oraz mniejszą głębokość niż oryginalny Circuit. Przy większej ilości czasu obliczeniowego można wykonać dodatkowe kroki optymalizacji, aby jeszcze bardziej zwiększyć wierność.

from scipy.optimize import OptimizeResult, minimize

from qiskit_addon_aqc_tensor.objective import MaximizeStateFidelity

objective = MaximizeStateFidelity(aqc_target_mps, aqc_ansatz, simulator_settings)

stopping_point = 1 - comparison_fidelity

def callback(intermediate_result: OptimizeResult):
print(f"Intermediate result: Fidelity {1 - intermediate_result.fun:.8}")
if intermediate_result.fun < stopping_point:
# Good enough for now
raise StopIteration

result = minimize(
objective.loss_function,
aqc_initial_parameters,
method="L-BFGS-B",
jac=True,
options={"maxiter": 100},
callback=callback,
)
if result.status not in (
0,
1,
99,
): # 0 => success; 1 => max iterations reached; 99 => early termination via StopIteration
raise RuntimeError(f"Optimization failed: {result.message} (status={result.status})")

print(f"Done after {result.nit} iterations.")
aqc_final_parameters = result.x
Intermediate result: Fidelity 0.95080335
Intermediate result: Fidelity 0.98408927
Intermediate result: Fidelity 0.99140876
Intermediate result: Fidelity 0.9951876
Intermediate result: Fidelity 0.99563147
Intermediate result: Fidelity 0.99646297
Intermediate result: Fidelity 0.99679298
Intermediate result: Fidelity 0.99715793
Intermediate result: Fidelity 0.99756604
Intermediate result: Fidelity 0.99804283
Intermediate result: Fidelity 0.99832283
Intermediate result: Fidelity 0.99856583
Intermediate result: Fidelity 0.99868698
Intermediate result: Fidelity 0.998867
Intermediate result: Fidelity 0.99902237
Intermediate result: Fidelity 0.99912174
Intermediate result: Fidelity 0.99919705
Intermediate result: Fidelity 0.99926724
Intermediate result: Fidelity 0.99938605
Intermediate result: Fidelity 0.99951297
Intermediate result: Fidelity 0.99956172
Intermediate result: Fidelity 0.99962274
Intermediate result: Fidelity 0.99963919
Intermediate result: Fidelity 0.99967423
Intermediate result: Fidelity 0.9997101
Done after 25 iterations.

Konstrukcja finalnego Circuit do przekazania do Transpilera

final_circuit = aqc_ansatz.assign_parameters(aqc_final_parameters)
final_circuit.compose(subsequent_circuit, inplace=True)
final_circuit.draw("mpl", fold=-1)

Quantum circuit diagram

Krok 2: Transpilacja do wykonania na docelowym sprzęcie

W Kroku 2 wzorca Qiskit transpilujemy ten Circuit i dowolne żądane obserwowalnych do wykonania na docelowym urządzeniu. Tutaj używamy fałszywego Backend dostarczonego przez qiskit-ibm-runtime.

from qiskit import transpile
from qiskit_ibm_runtime.fake_provider import FakeMelbourneV2

backend = FakeMelbourneV2()

isa_circuit = transpile(final_circuit, backend)
isa_observable = observable.apply_layout(isa_circuit.layout)

Wynikowy Circuit ISA może być następnie wysłany do wykonania na Backend (krok 3 wzorca Qiskit).

Krok 3: Wykonanie na sprzęcie kwantowym

from qiskit_ibm_runtime import EstimatorV2 as Estimator

estimator = Estimator(backend)
job = estimator.run([(isa_circuit, isa_observable)])
pub_result = job.result()[0]

Krok 4: Rekonstrukcja

Rekonstrukcja nie jest w naszym przypadku konieczna. Możemy po prostu spojrzeć na wynik.

pub_result.data.evs[()]
np.float64(0.047998046875000006)