Przejdź do głównej treści

Bringing it all together with Qiskit Runtime

Summary

Victoria Lipinska przedstawia końcowe podsumowanie tego, czego się do tej pory nauczyliśmy.

References

Poniższe artykuły są przywoływane w powyższym filmie.

VQE with Qiskit patterns

Mamy wszystkie niezbędne elementy do przeprowadzenia obliczenia VQE:

  • Hamiltonian
  • Ansatz
  • Klasyczny optymalizator

Teraz wystarczy połączyć je w ramach frameworku Qiskit patterns.

Step 1: Map classical inputs to a quantum problem

Jak wspomniano wcześniej, zakładamy tutaj, że odpowiednio sformatowany Hamiltonian interesującej nas układu został już wygenerowany. Jeśli masz pytania na ten temat, zajrzyj do lekcji o Hamiltonianach. Poniższy blok kodu konfiguruje komponenty omówione w poprzednich lekcjach. Wybraliśmy tu modelowanie H2, ponieważ jego Hamiltonian jest na tyle zwarty, że można go zapisać wprost.

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-aer qiskit-ibm-runtime scipy
# General imports
import numpy as np
from qiskit.quantum_info import SparsePauliOp

# Hamiltonian obtained from a previous lesson

H = SparsePauliOp(
[
"IIII",
"IIIZ",
"IZII",
"IIZI",
"ZIII",
"IZIZ",
"IIZZ",
"ZIIZ",
"IZZI",
"ZZII",
"ZIZI",
"YYYY",
"XXYY",
"YYXX",
"XXXX",
],
coeffs=[
-0.09820182 + 0.0j,
-0.1740751 + 0.0j,
-0.1740751 + 0.0j,
0.2242933 + 0.0j,
0.2242933 + 0.0j,
0.16891402 + 0.0j,
0.1210099 + 0.0j,
0.16631441 + 0.0j,
0.16631441 + 0.0j,
0.1210099 + 0.0j,
0.17504456 + 0.0j,
0.04530451 + 0.0j,
0.04530451 + 0.0j,
0.04530451 + 0.0j,
0.04530451 + 0.0j,
],
)

nuclear_repulsion = 0.7199689944489797

Na początek wybieramy obwód efficient_su2 oraz optymalizator COBYLA.

# Pre-defined ansatz circuit
from qiskit.circuit.library import efficient_su2

# SciPy minimizer routine
from scipy.optimize import minimize

# Plotting functions

# Random initial state and efficient_su2 ansatz
ansatz = efficient_su2(H.num_qubits, su2_gates=["rx"], entanglement="linear", reps=1)
x0 = 2 * np.pi * np.random.random(ansatz.num_parameters)
print(ansatz.decompose().depth())
ansatz.decompose().draw("mpl")
5

Output of the previous code cell

Teraz konstruujemy naszą funkcję kosztu. Jest ona oczywiście powiązana z Hamiltonianem, ale różni się od niego tym, że Hamiltonian jest operatorem, a my chcemy funkcji zwracającej wartość oczekiwaną tego operatora — korzystając z Estimatora. Oczywiście realizuje to za pomocą ansatzu i parametrów wariacyjnych, więc wszystkie te elementy pojawiają się jako argumenty. Poniżej definiujemy nieco różne wersje do użycia na prawdziwym sprzęcie lub symulatorach.

def cost_func(params, ansatz, H, estimator):
pub = (ansatz, [H], [params])
result = estimator.run(pubs=[pub]).result()
energy = result[0].data.evs[0]
return energy

# def cost_func_sim(params, ansatz, H, estimator):
# energy = estimator.run(ansatz, H, parameter_values=params).result().values[0]
# return energy

Krok 2: Zoptymalizuj problem pod kątem wykonania kwantowego.

Chcemy, żeby nasz kod działał jak najwydajniej na używanym sprzęcie. Dlatego musimy wybrać backend, aby rozpocząć etap optymalizacji. Poniższy kod wybiera najmniej obciążony backend dostępny dla Ciebie.

# To run on hardware, select the backend with the fewest number of jobs in the queue
from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService(channel="ibm_quantum_platform")
backend = service.least_busy(operational=True, simulator=False)
backend.name

Optymalizacja Circuit pod kątem uruchomienia na prawdziwym backendzie to obszerny i kluczowy temat. Nie jest on jednak specyficzny dla VQE. Na razie przypomnimy Ci jedynie dwa ważne pojęcia:

  • optimization_level: Opisuje, jak dobrze Circuit jest dopasowany do układu wybranego backendu. Najniższy poziom optymalizacji robi absolutne minimum potrzebne do uruchomienia Circuit na urządzeniu — mapuje Qubit/kubity Circuit na Qubit/kubity urządzenia i dodaje bramki swap, aby umożliwić wszystkie operacje dwu-qubitowe. Najwyższy poziom optymalizacji jest znacznie inteligentniejszy i używa wielu sztuczek, by zmniejszyć całkowitą liczbę Gate'ów. Ponieważ bramki wieloqubitowe mają wysokie wskaźniki błędów, a Qubit/kubity z czasem tracą koherencję, krótsze Circuit powinny dawać lepsze wyniki.
  • Dynamical Decoupling: Możemy zastosować sekwencję Gate'ów do bezczynnych Qubit/kubitów. Pozwala to wyeliminować część niepożądanych interakcji ze środowiskiem. Zapoznaj się z powiązaną dokumentacją, aby uzyskać więcej informacji na temat optymalizacji Circuit. Poniższy kod generuje menedżer przejść przy użyciu wstępnie zdefiniowanych menedżerów przejść z qiskit.transpiler.
from qiskit.transpiler import PassManager
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.transpiler.passes import (
ALAPScheduleAnalysis,
PadDynamicalDecoupling,
ConstrainedReschedule,
)
from qiskit.circuit.library import XGate

target = backend.target
pm = generate_preset_pass_manager(target=target, optimization_level=3)
pm.scheduling = PassManager(
[
ALAPScheduleAnalysis(target=target),
ConstrainedReschedule(
acquire_alignment=target.acquire_alignment,
pulse_alignment=target.pulse_alignment,
target=target,
),
PadDynamicalDecoupling(
target=target,
dd_sequence=[XGate(), XGate()],
pulse_alignment=target.pulse_alignment,
),
]
)

# Use the pass manager and draw the resulting circuit
ansatz_isa = pm.run(ansatz)
ansatz_isa.draw(output="mpl", idle_wires=False, style="iqp")

Output of the previous code cell

Musimy podobnie zastosować charakterystyki układu urządzenia do Hamiltonianu.

hamiltonian_isa = H.apply_layout(ansatz_isa.layout)
hamiltonian_isa
SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYYYII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYXXII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXYYII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXXXII'],
coeffs=[-0.09820182+0.j, -0.1740751 +0.j, -0.1740751 +0.j, 0.2242933 +0.j,
0.2242933 +0.j, 0.16891402+0.j, 0.1210099 +0.j, 0.16631441+0.j,
0.16631441+0.j, 0.1210099 +0.j, 0.17504456+0.j, 0.04530451+0.j,
0.04530451+0.j, 0.04530451+0.j, 0.04530451+0.j])

Step 3: Execute using Qiskit Primitives.

Zanim uruchomimy obliczenia na wybranym sprzęcie, warto skorzystać z symulatora do wstępnego debugowania, a czasem do szacowania błędów. Z tych powodów pokrótce pokazujemy, jak uruchomić VQE na symulatorze. Należy jednak koniecznie pamiętać, że żaden klasyczny komputer, symulator ani GPU nie jest w stanie dokładnie zasymulować pełnej funkcjonalności silnie splątanego komputera kwantowego o 127 qubitach. W obecnej erze użyteczności kwantowej symulatory mają ograniczone zastosowanie.

Przypomnij sobie, że dla każdego zestawu parametrów w obwodzie wariacjonalnym należy obliczyć wartość oczekiwaną (ponieważ to właśnie ona jest minimalizowana). Jak pewnie już się domyślasz, najefektywniejszym sposobem jest użycie prymitywu Qiskit — Estimator. Zaczniemy od lokalnego symulatora, co wymaga użycia lokalnej wersji Estimator o nazwie BackendEstimator.

Zachowując prawdziwy backend używany do optymalizacji, możemy zaimportować model zachowania szumów tego urządzenia, a następnie użyć go z wybranym lokalnym symulatorem. Tutaj skorzystamy z aer_simulator_statevector.

# We will start by using a local simulator
from qiskit_aer import AerSimulator

# Import an estimator, this time from qiskit (we will import from Runtime for real hardware)
from qiskit.primitives import BackendEstimatorV2

# generate a simulator that mimics the real quantum system
backend_sim = AerSimulator.from_backend(backend)
estimator = BackendEstimatorV2(backend=backend_sim)

Nadszedł wreszcie czas na implementację VQE — minimalizowanie funkcji kosztu przy użyciu wybranego Hamiltonianu, ansatz, klasycznego optymalizatora oraz naszego BackendEstimator, opartego na prawdziwym backendzie wybranym do dalszego użytku. Zauważ, że tutaj wybraliśmy stosunkowo małą liczbę maksymalnych iteracji. Wynika to z tego, że używamy symulatora jedynie do debugowania. Kroki optymalizacji VQE często wymagają setek iteracji, aby osiągnąć zbieżność.

res = minimize(
cost_func,
x0,
args=(ansatz_isa, hamiltonian_isa, estimator),
method="cobyla",
options={"maxiter": 10, "disp": True},
)

print(getattr(res, "fun") - nuclear_repulsion)
print(res)
Return from COBYLA because the objective function has been evaluated MAXFUN times.
Number of function values = 10 Least value of F = -0.11556938907226563
The corresponding X is:
[4.11796514 4.52126324 0.69570423 4.12781503 6.55507846 1.80713073
0.9645473 6.23812214]

-0.8355383835212453
message: Return from COBYLA because the objective function has been evaluated MAXFUN times.
success: False
status: 3
fun: -0.11556938907226563
x: [ 4.118e+00 4.521e+00 6.957e-01 4.128e+00 6.555e+00
1.807e+00 9.645e-01 6.238e+00]
nfev: 10
maxcv: 0.0

Ten kod wykonał się poprawnie, choć nie osiągnął zbieżności — czego się spodziewaliśmy. Przejdziemy teraz do uruchomienia obliczeń na prawdziwym sprzęcie, a następnie omówimy wyniki. W przypadku prawdziwych backendów użyjemy Estimator z Qiskit Runtime. Będziemy chcieli uruchomić go wewnątrz sesji Qiskit Runtime i zazwyczaj będziemy chcieli określić opcje dla tej sesji.

from qiskit_ibm_runtime import QiskitRuntimeService, Session
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_ibm_runtime.options import EstimatorOptions

Używanie sesji oznacza między innymi, że nasze zadanie tylko raz czeka w kolejce — na początku. Kolejne iteracje klasycznego optymalizatora nie będą kolejkowane. W sesji możemy ustawić poziomy odporności na błędy i optymalizacji. Te narzędzia są na tyle ważne, że zamieszczamy krótkie omówienie każdego z nich oraz ich znaczenia w VQE, wraz z odnośnikami do dalszej lektury:

  • Sesje Runtime: VQE jest z natury iteracyjne — klasyczny optymalizator wybiera nowe parametry wariacjonalne, a tym samym nowe Gate'y w każdej kolejnej próbie. Bez użycia sesji mogłoby to powodować dodatkowy czas oczekiwania w kolejce między każdym próbnym obwodem. Ujęcie obliczeń VQE w sesję skutkuje tylko jedną początkową kolejką przed startem zadania, bez dodatkowego czasu oczekiwania między krokami wariacjonalnymi. Ta strategia była już stosowana w przykładzie z poprzedniej lekcji, ale może odgrywać jeszcze ważniejszą rolę przy zmianie geometrii. Więcej o sesjach znajdziesz w dokumentacji trybów wykonania.
  • Wbudowana optymalizacja Estimator: W Estimator dostępne są wbudowane opcje optymalizacji obliczeń. W wielu kontekstach (w tym w Estimator) ustawienia są ograniczone do 0 i 1, gdzie 0 oznacza brak optymalizacji, a 1 (domyślne) oznacza pewną optymalizację obwodu pod kątem wybranego sprzętu. Niektóre inne konteksty dopuszczają wartości 0, 1, 2 lub 3. Więcej o konkretnych metodach stosowanych przy różnych ustawieniach znajdziesz w dokumentacji. Tutaj ustawimy optymalizację na 0 i użyjemy 'skip\_transpilation = true', ponieważ nasz Circuit został już transpilowany przy użyciu menedżera przejść w sekcji optymalizacji powyżej.
  • Wbudowana odporność Estimator: Podobnie jak w przypadku optymalizacji, Estimator ma wbudowane ustawienia odporności na błędy, odpowiadające różnym podejściom do mitygacji błędów. Aby dowiedzieć się więcej o ustawieniach poziomu odporności, zajrzyj do dokumentacji.

Warto zauważyć, że mitygacja błędów odgrywa subtelną rolę w zbieżności obliczeń VQE. Klasyczny optymalizator przeszukuje przestrzeń parametrów w poszukiwaniu tych, które minimalizują energię. Gdy jesteś bardzo daleko od optymalnych parametrów, stromy gradient może być widoczny dla klasycznego optymalizatora nawet w obecności błędów. Jednak w miarę jak obliczenia zbiegają się i zbliżasz się do optymalnych wartości, gradient staje się coraz mniejszy i łatwiej go zagłuszyć przez błędy. Ile mitygacji błędów chcesz zastosować? W jakich momentach zbieżności? To są wybory, które musisz podjąć dla swojego konkretnego przypadku użycia.

Dla tego pierwszego uruchomienia na sprzęcie ustawiliśmy odporność na 0, aby umożliwić stosunkowo szybkie działanie. W przypadku każdej poważnej aplikacji będziesz chciał(-a) używać mitygacji błędów. Zauważ, że w poniższej komórce znajdują się dwa zestawy opcji: (1) opcje dla sesji Runtime, które nazwaliśmy "session\_options", oraz (2) opcje dla klasycznego optymalizatora, tutaj po prostu nazwane "options".

estimator_options = EstimatorOptions(resilience_level=0, default_shots=2000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)

res = minimize(
cost_func,
x0,
args=(ansatz_isa, hamiltonian_isa, estimator),
method="cobyla",
options={"maxiter": 10, "disp": True},
)
Return from COBYLA because the objective function has been evaluated MAXFUN times.
Number of function values = 10 Least value of F = -0.11691688904
The corresponding X is:
[5.11796514 5.52126324 0.69570423 5.12781503 6.55507846 1.80713073
1.9645473 6.23812214]

Możesz śledzić postęp swojego zadania na platformie IBM Quantum® Platform w sekcji Workloads.

print(getattr(res, "fun") - nuclear_repulsion)
print(res)
-0.8368858834889796
message: Return from COBYLA because the objective function has been evaluated MAXFUN times.
success: False
status: 3
fun: -0.11691688904
x: [ 5.118e+00 5.521e+00 6.957e-01 5.128e+00 6.555e+00
1.807e+00 1.965e+00 6.238e+00]
nfev: 10
maxcv: 0.0

Krok 4: Przetwarzanie końcowe, zwrócenie wyniku w klasycznym formacie.

Zatrzymajmy się na chwilę, żeby upewnić się, że rozumiemy te wyniki. Wartość „fun" to minimalna wartość, jaką uzyskaliśmy dla funkcji kosztu (niekoniecznie ostatnia obliczona wartość). Jest to energia całkowita, uwzględniająca dodatnią energię odpychania jądrowego — właśnie dlatego zdefiniowaliśmy również electron_energy.

W powyższym przypadku pojawia się komunikat, że przekroczono maksymalną liczbę wywołań funkcji, a liczba wywołań funkcji (nfev) wynosiła 10. Oznacza to po prostu, że inne kryteria zbieżności optymalizacji nie zostały spełnione — innymi słowy, nie ma powodów sądzić, że znaleźliśmy energię stanu podstawowego. Takie też jest znaczenie wartości success równej „False".

Na koniec mamy x. To wektor parametrów wariacyjnych — parametry użyte w obliczeniach, które dały minimalną funkcję kosztu (wartość oczekiwaną energii). Te osiem wartości odpowiada ośmiu kątom obrotu w bramkach ansatz, które przyjmują zmienne kąty obrotu.

Gratulacje! Udało ci się uruchomić obliczenia VQE na QPU IBM Quantum!

W kolejnej lekcji zobaczymy, jak dostosować ten przepływ pracy, aby uwzględniał zmienne w Hamiltonianie. W kontekście problemów chemii kwantowej może to oznaczać na przykład zmianę geometrii w celu wyznaczenia kształtów cząsteczek lub miejsc wiązania.

import qiskit
import qiskit_ibm_runtime

print(qiskit.version.get_version_info())
print(qiskit_ibm_runtime.version.get_version_info())
2.1.0
0.40.1