Przejdź do głównej treści

Eksperyment skali użytkowej I

uwaga

Tamiya Onodera (5 lipca 2024)

Pobierz plik pdf oryginalnego wykładu. Należy pamiętać, że niektóre fragmenty kodu mogą być przestarzałe, ponieważ są to obrazy statyczne.

Przybliżony czas QPU potrzebny do uruchomienia tego eksperymentu wynosi 45 sekund.

1. Wprowadzenie do artykułu o użyteczności

W tej lekcji uruchamiamy obwód skali użytkowej, który pojawia się w pracy, którą nieformalnie nazywamy "artykułem o użyteczności", opublikowanej w Nature Vol 618, 15 czerwca 2023. Artykuł dotyczy ewolucji czasowej dwuwymiarowego modelu Isinga w polu poprzecznym. W szczególności rozpatrują oni dynamikę czasową Hamiltonianu,

H=HZZ+HX=J(i,j)ZiZj+hiXiH = H_{ZZ} + H_X = - J \sum_{(i,j)} Z_i Z_j + h \sum_{i} X_i

w którym J>0J > 0 jest sprzężeniem najbliższych sąsiednich spinów z i<ji < j, a hh jest globalnym polem poprzecznym. Symulują oni dynamikę spinów ze stanu początkowego za pomocą rozkładu Trottera pierwszego rzędu operatora ewolucji czasowej,

exp(iHZZδt)=(i,j)exp(iJδtZiZj)=(i,j)RZiZj(2Jδt)exp(iHXδt)=iexp(ihδtXi)=iRXi(2hδt)\begin{aligned} \exp(-i H_{ZZ} \delta t) &= \prod_{(i,j)} \exp (i J \delta t Z_i Z_j) = \prod_{(i,j)} \mathrm{R}_{Z_i Z_j} ( - 2 J \delta t) \\ \exp(-i H_X \delta t) &= \prod_{i} \exp (-i h \delta t X_i ) = \prod_{i} \mathrm{R}_{X_i} ( 2 h \delta t) \end{aligned}

w którym czas ewolucji TT jest dyskretyzowany na T/δtT / \delta t Trotter steps, a RZiZj(θJ)\mathrm{R}_{Z_i Z_j}(\theta_J) i RXi(θh)\mathrm{R}_{X_i}(\theta_h) to bramki rotacji ZZZZ i XX, odpowiednio.

Przeprowadzili oni eksperymenty na procesorze IBM Quantum® Eagle, który jest urządzeniem 127-kubitowym o łączności heavy-hex, stosując oddziaływania XX do wszystkich kubitów i oddziaływania ZZZZ dla wszystkich krawędzi mapy sprzężeń. Należy zauważyć, że nie wszystkie oddziaływania ZZZZ mogą być stosowane jednocześnie z powodu "zależności danych". Dlatego kolorują oni mapę sprzężeń, aby pogrupować je w warstwy. Te w jednej warstwie otrzymują ten sam kolor i mogą być stosowane równolegle.

Dodatkowo, dla uproszczenia eksperymentu, skupili się na przypadku θJ=π/2\theta_J=-\pi /2.

Nowatorskim wkładem artykułu jest to, że zbudowali oni obwody kwantowe w skali wykraczającej poza symulację wektora stanu, uruchomili je na szumnych komputerach kwantowych i z sukcesem wyekstrahowali wiarygodne wyniki. Oznacza to, że zademonstrowali oni użyteczność szumnych komputerów kwantowych. Czyniąc to, zastosowali ekstrapolację do zerowego szumu (ZNE) z probabilistycznym wzmocnieniem błędów (PEA), aby łagodzić błędy z szumnych urządzeń.

Od tego czasu takie eksperymenty i obwody nazywamy "skali użytkowej".

1.1 Twój cel

Twoim celem w tej lekcji jest zbudowanie obwodu skali użytkowej i uruchomienie go na procesorze Eagle. Uzyskanie wiarygodnych wyników wykracza poza zakres tego notatnika, częściowo dlatego, że PEA jest funkcją eksperymentalną Qiskit w czasie pisania tego tekstu, a częściowo dlatego, że zastosowanie ZNE z PEA zajmie sporo czasu.

Konkretnie, jesteś proszony o zbudowanie i uruchomienie obwodu odpowiadającego Rysunkowi 4b artykułu oraz narysowanie własnych "niezłagodzonych" punktów. Jak widzisz, jest to obwód 127-kubitowy ×\times 60-warstwowy (20 Trotter steps) z Z62\langle Z_{62} \rangle jako obserwablą. image.png Brzmi poważnie?   Nie martw się. Ostatnie trzy lekcje tego kursu stanowią kamienie milowe. Na początek zademonstrujemy eksperyment mniejszej skali, który polega na zbudowaniu i uruchomieniu na urządzeniu symulowanym obwodu 27-kubitowego ×\times 6-warstwowego (2 Trotter steps) z Z13\langle Z_{13} \rangle jako obserwablą.

To wszystko, jeśli chodzi o wprowadzenie. Wyruszmy na przygodę skali użytkowej!

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime rustworkx
import qiskit

qiskit.__version__
'2.0.2'
#!pip install qiskit_ibm_runtime
#!pip install qiskit_aer
import matplotlib.pyplot as plt
import numpy as np
import rustworkx as rx

from qiskit import QuantumCircuit, transpile
from qiskit.circuit import Parameter
from qiskit.circuit.library import YGate
from qiskit.quantum_info import SparsePauliOp
from qiskit_ibm_runtime import (
QiskitRuntimeService,
fake_provider,
EstimatorV2 as Estimator,
)
from qiskit_aer import AerSimulator
service = QiskitRuntimeService()

2. Przygotowanie

2.1 Konstrukcja RZZ(-π\pi / 2)

Najpierw zauważmy, że bramka RZZ w ogólności wymaga dwóch bramek CXCX.

from qiskit.circuit.library import RZZGate

θ_h = Parameter("$\\theta_h$")
qc1 = QuantumCircuit(2)
qc1.append(RZZGate(θ_h), [0, 1])
qc1.decompose(reps=1).draw("mpl")

Output of the previous code cell

Jak wspomniano powyżej, w tym eksperymencie skupiamy się na bramce RZZ z konkretnym kątem -π\pi / 2. Jak pokazano w artykule, można ją zrealizować przy użyciu tylko jednej bramki CXCX.

qc2 = QuantumCircuit(2)

qc2.sdg([0, 1])
qc2.append(YGate().power(1 / 2), [1])
qc2.cx(0, 1)
qc2.append(YGate().power(1 / 2).adjoint(), [1])

qc2.draw("mpl")

Output of the previous code cell

Definiujemy bramkę na podstawie tego obwodu do wykorzystania w przyszłości.

rzz = qc2.to_gate(label="RZZ")

Zróbmy dowolny użytek z nowo zdefiniowanej rzz.

qc3 = QuantumCircuit(3)
qc3.append(rzz, [0, 1])
qc3.append(rzz, [0, 2])
display(qc3.draw("mpl"))
# display(qc.decompose(reps=1).draw("mpl"))

Output of the previous code cell

Zanim użyjemy tego dalej, zweryfikujmy logiczną równoważność qc1 (bramki RZZ) dla -pi/2 oraz naszej nowo zdefiniowanej bramki rzz lub qc2:

from qiskit.quantum_info import Operator

op1 = Operator(qc1.assign_parameters([-np.pi / 2]))
op2 = Operator(qc2)

op1.equiv(op2)
True

2.2 Kolorowanie mapy sprzężeń

Przeanalizujmy, jak kolorujemy mapę sprzężeń backendu. Jest to potrzebne do grupowania interakcji ZZZZ w warstwy.

Na początek zwizualizujmy mapę sprzężeń backendu. Zauważ, że mapy sprzężeń są typu heavy-hexagonal dla wszystkich obecnych urządzeń IBM Quantum.

backend = service.least_busy(operational=True, simulator=False)

backend.coupling_map.draw()

Output of the previous code cell

Do kolorowania mapy sprzężeń używamy rustworkx, pakietu Pythona do pracy z grafami i sieciami złożonymi. Udostępnia on wiele algorytmów kolorowania, z których wszystkie są heurystyczne, a zatem nie gwarantują znalezienia minimalnego kolorowania.

Mając to na uwadze, ponieważ grafy heavy-hex są dwudzielne, wybieramy graph_bipartite_edge_color, który powinien znaleźć minimalne kolorowanie dla tych grafów.

def color_coupling_map(backend):
graph = backend.coupling_map.graph
undirected_graph = graph.to_undirected(multigraph=False)
edge_color_map = rx.graph_bipartite_edge_color(undirected_graph)
if edge_color_map is None:
edge_color_map = rx.graph_greedy_edge_color(undirected_graph)
# build a map from color to a list of edges
edge_index_map = undirected_graph.edge_index_map()
color_edges_map = {color: [] for color in edge_color_map.values()}
for edge_index, color in edge_color_map.items():
color_edges_map[color].append(
(edge_index_map[edge_index][0], edge_index_map[edge_index][1])
)
return edge_color_map, color_edges_map

Grafy heavy-hexagonal powinny być pokolorowane trzema kolorami. Sprawdźmy to dla powyższej mapy sprzężeń.

edge_color_map, color_edges_map = color_coupling_map(backend)
print(
f"{backend.name}, {backend.num_qubits}-qubit device, {len(color_edges_map.keys())} colors assigned."
)
ibm_strasbourg, 127-qubit device, 3 colors assigned.

Tak, zgadza się!

Dla zabawy pomalujmy mapę sprzężeń zgodnie z uzyskanym kolorowaniem, używając funkcji wizualizacji rustworkx.

color_str_map = {0: "green", 1: "red", 2: "blue"}

undirected_graph = backend.coupling_map.graph.to_undirected(multigraph=False)
for i in undirected_graph.edge_indices():
undirected_graph.get_edge_data_by_index(i)["color"] = color_str_map[
edge_color_map[i]
]

rx.visualization.graphviz_draw(
undirected_graph, method="neato", edge_attr_fn=lambda edge: {"color": edge["color"]}
)

Output of the previous code cell

3. Rozwiąż ewolucję czasową metodą Trottera dla dwuwymiarowego modelu Isinga.

Zdefiniujmy procedurę konstruującą obwód z artykułu o użyteczności (utility paper) służący do ewolucji czasowej dwuwymiarowego modelu Isinga. Procedura przyjmuje trzy parametry: backend, liczbę całkowitą wskazującą liczbę kroków Trottera oraz wartość logiczną kontrolującą wstawianie barier.

def get_utility_circuit(backend, num_steps: int, barrier: bool = False):
num_qubits = backend.num_qubits
_, color_edges_map = color_coupling_map(backend)
θ_h = Parameter("$\\theta_h$")
qc = QuantumCircuit(num_qubits)

for i in range(num_steps):
qc.rx(θ_h, range(num_qubits))

for _, edge_list in color_edges_map.items():
for edge in edge_list:
qc.append(rzz, edge)

if barrier:
qc.barrier()
return qc

Zwróć uwagę, że ręcznie wykonaliśmy już mapowanie i trasowanie kubitów dla skonstruowanego obwodu. Dlatego, gdy później transpilujemy obwód, nie prosimy (nie powinniśmy prosić) transpilera o wykonanie mapowania i trasowania kubitów. Jak wkrótce zobaczysz, wywołujemy go z poziomem optymalizacji równym 1 oraz metodą układu (layout method) "trivial".

Następnie definiujemy prostą procedurę uzyskującą informacje o skonstruowanym obwodzie w celu szybkiej weryfikacji.

def get_circuit_info(qc: QuantumCircuit, reps: int = 0):
qc0 = qc.decompose(reps=reps)
return (
f"{qc0.num_qubits} qubits × {qc0.depth(lambda x: x.operation.num_qubits == 2)} layers ({qc0.depth()}-depth)"
+ ", "
+ f"""Gate breakdown: {", ".join([f"{k.upper()} {v}" for k, v in qc0.count_ops().items()])}"""
)

Wykorzystajmy te procedury. Powinieneś zobaczyć obwód o rozmiarze 27 kubitów ×\times 15 warstw (5 kroków Trottera). Ponieważ fałszywe urządzenie ma 28 krawędzi, powinno być 28*5 bramek splątujących.

backend = fake_provider.FakeTorontoV2()
num_steps = 5
qc = get_utility_circuit(backend, num_steps, True)

display(qc.draw(output="mpl", fold=-1))
print(get_circuit_info(qc, reps=0))
print(get_circuit_info(qc, reps=1))

Wynik poprzedniej komórki kodu

27 qubits × 15 layers (20-depth),  Gate breakdown: CIRCUIT-165 140, RX 135, BARRIER 5
27 qubits × 15 layers (60-depth), Gate breakdown: SDG 280, UNITARY 280, CX 140, R 135, BARRIER 5

4. Rozwiąż 27-kubitową wersję problemu.

Teraz zademonstrujemy mniejszą wersję eksperymentu użyteczności. Zbudujemy obwód 27-kubitowy ×\times 6 warstw (2 Trotter steps) z Z13\langle Z_{13} \rangle jako obserwablą i uruchomimy go zarówno na AerSimulator, jak i na urządzeniu fake.

Oczywiście kierujemy się naszym czterostopniowym schematem pracy, "Qiskit pattern", który składa się z etapów Map, Optimize, Execute i Post-Process. Bardziej konkretnie:

  • Map – odwzorowanie klasycznych danych wejściowych na obliczenia kwantowe.
  • Optimize – optymalizacja obwodów na potrzeby obliczeń kwantowych.
  • Execute – wykonanie obwodów z użyciem prymitywów.
  • Post-process – postprzetwarzanie i zwrócenie wyników w formacie klasycznym.

Poniżej wykonujemy etap Map, tworząc obwód do eksperymentu w mniejszej skali. Następnie mamy jeden zestaw Optimize i Execute dla AerSimulator oraz drugi dla urządzenia fake. Na koniec wykonujemy etap Post-Process, aby wykreślić wyniki.

4.1 Krok 1: Map

backend = fake_provider.FakeTorontoV2()  # a 27 qubit fake device.
num_steps = 2
qc = get_utility_circuit(backend, num_steps)
obs = SparsePauliOp.from_sparse_list(
[("Z", [13], 1)], num_qubits=backend.num_qubits
) # Falcon
angles = [
0,
0.1,
0.2,
0.3,
0.4,
0.5,
0.6,
0.7,
0.8,
1.0,
np.pi / 2,
] # We try 11 angles for theta_h.

4.2 Kroki 2 i 3: Optimize i Execute (Simulator)

backend_sim = AerSimulator()
transpiled_qc_sim = transpile(
qc, backend_sim, optimization_level=1, layout_method="trivial"
)
transpiled_obs_sim = obs.apply_layout(layout=transpiled_qc_sim.layout)

print(get_circuit_info(qc, reps=1))
print(get_circuit_info(transpiled_qc_sim, reps=1))
27 qubits × 6 layers (23-depth),  Gate breakdown: SDG 112, UNITARY 112, CX 56, R 54
27 qubits × 6 layers (16-depth), Gate breakdown: U3 80, CX 56, R 54, U1 32, U 28

Pewien użytkownik uruchomił kolejną komórkę na MacBooku Pro z czterordzeniowym procesorem Intel Core i7 2,3 GHz wyposażonym w 32 GB pamięci 3LPDDR4X RAM, z systemem macOS 14.5. Zajęło to 161 ms czasu rzeczywistego (wall time). Każdy laptop będzie się nieco różnił.

%%time
params = [[p] for p in angles]
estimator = Estimator(mode=backend_sim)
pub = (transpiled_qc_sim, transpiled_obs_sim, params)
result_sim = estimator.run([pub]).result()
CPU times: user 231 ms, sys: 186 ms, total: 417 ms
Wall time: 111 ms

4.3 Kroki 2 i 3: Optimize i execute (urządzenie fake)

backend_fake = fake_provider.FakeTorontoV2()
transpiled_qc_fake = transpile(
qc, backend_fake, optimization_level=1, layout_method="trivial"
)
transpiled_obs_fake = obs.apply_layout(layout=transpiled_qc_fake.layout)

print(get_circuit_info(qc, reps=1))
print(get_circuit_info(transpiled_qc_fake, reps=1))
27 qubits × 6 layers (23-depth),  Gate breakdown: SDG 112, UNITARY 112, CX 56, R 54
27 qubits × 6 layers (49-depth), Gate breakdown: SDG 324, U1 274, H 162, CX 56, U3 14

Gdy ten sam użytkownik uruchomił kolejną komórkę w tym samym środowisku co powyżej, zajęło to 2 min 19 s czasu rzeczywistego (wall time). Wykonanie obwodu na urządzeniu fake uruchamia symulację z szumem, która zajmuje znacznie więcej czasu niż symulacja dokładna. Zalecamy, aby nie uruchamiać większego obwodu (na przykład 27-kubitowego ×\times 9 warstw z 3 Trotter steps) na urządzeniu fake.

%%time
params = [[p] for p in angles]
estimator = Estimator(mode=backend_fake)
pub = (transpiled_qc_fake, transpiled_obs_fake, params)
result_fake = estimator.run([pub]).result()
CPU times: user 4min 42s, sys: 9.35 s, total: 4min 51s
Wall time: 38.3 s

4.4 Krok 4: Post-process

Wykreślamy wyniki z symulacji dokładnej i zaszumionej. Widać poważny wpływ szumu na FakeToronto.

plt.plot(angles, result_fake[0].data.evs, "o", label="Fake Device")
plt.plot(angles, result_sim[0].data.evs, "o", label="AerSimulator")
plt.xlabel("$\\mathrm{R_x}$ angle $\\theta_h$")
plt.title("$\\langle Z_{13} \\rangle$")
plt.legend()
plt.show()

Wynik poprzedniej komórki kodu

5. Rozwiąż 127-kubitową wersję problemu

Twoim celem jest uruchomienie eksperymentu na skalę użyteczności, o którym wspominaliśmy na początku. Utworzysz i wykonasz obwód 127-qubit i 60-warstwowy (20 kroków Trotter) z Z62\langle Z_{62} \rangle jako observable. Zalecamy, abyś spróbował zrobić to samodzielnie, korzystając w odpowiednich miejscach z kodu dla wersji 27-kubitowej. Rozwiązanie jest jednak podane poniżej.

Rozwiązanie:

5.1 Krok 1: Mapowanie

# backend_map = service.backend("ibm_brisbane")
backend_map = service.least_busy(operational=True, simulator=False)

num_steps = 20
qc = get_utility_circuit(backend_map, num_steps)
obs = SparsePauliOp.from_sparse_list(
[("Z", [62], 1)], num_qubits=backend_map.num_qubits
) # Eagle
angles = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 1.0, np.pi / 2]

5.2 Kroki 2 i 3: Optymalizacja i wykonanie

Zauważmy, że mapa sprzężeń procesora Eagle ma 144 krawędzie.

# backend = service.backend("ibm_brisbane")
backend = backend_map

transpiled_qc = transpile(qc, backend, optimization_level=1, layout_method="trivial")
transpiled_obs = obs.apply_layout(layout=transpiled_qc.layout)

print(get_circuit_info(qc, reps=1))
print(get_circuit_info(transpiled_qc))
156 qubits × 60 layers (221-depth),  Gate breakdown: SDG 7040, UNITARY 7040, CX 3520, R 3120
156 qubits × 60 layers (201-depth), Gate breakdown: RZ 11933, SX 6240, CZ 3520
params = [[p] for p in angles]
estimator = Estimator(mode=backend)
pub = (transpiled_qc, transpiled_obs, params)
job = estimator.run([pub])

job_id = job.job_id()
print(f"job id={job_id}")
job id=d1479n6qf56g0081sxa0

5.3 Przetwarzanie końcowe

Podajemy wartości dla punktów „mitigated” z Rysunku 4b z utility paper. Wykreśl je razem ze swoimi wynikami.

result_paper = [
1.0171,
1.0044,
0.9563,
0.9602,
0.8394,
0.8120,
0.5466,
0.4556,
0.1953,
0.0141,
0.0117,
]

# REPLACE WITH YOUR OWN JOB ID
job = service.job(job_id)

plt.plot(angles, job.result()[0].data.evs, "o", label=f"{job.backend().name}")
plt.plot(angles, result_paper, "o", label="Utility Paper")
plt.xlabel("$\\mathrm{R_x}$ angle $\\theta_h$")
plt.title("$\\langle Z_{62} \\rangle$")
plt.legend()
plt.show()

Output of the previous code cell

Czy twoje wyniki są podobne do „unmitigated” z Rysunku 4b?   Mogą być bardzo różne, w zależności od urządzenia i jego stanu w momencie eksperymentu. Nie martw się samymi wynikami. Sprawdzimy, czy poprawnie napisałeś kod. Jeśli tak, gratulacje – dotarłeś do linii startu ery użyteczności.

Podobnie jak w utility paper, naukowcy na całym świecie wykazali się ogromną pomysłowością w wydobywaniu znaczących wyników nawet w obecności szumu. Ostatecznym celem tego zbiorowego wysiłku jest przewaga kwantowa: stan, w którym komputery kwantowe potrafią rozwiązywać pewne problemy użyteczne w przemyśle szybciej, z większą wiernością lub taniej niż komputery klasyczne. Prawdopodobnie nie będzie to pojedyncze wydarzenie, lecz raczej era, w której klasyczne odtworzenie wyników kwantowych zajmuje coraz dłużej, aż w pewnym momencie ta przewaga czasowa kwantowa staje się kluczowo istotna. Jedno jest pewne co do przewagi kwantowej: dojdziemy do niej wyłącznie poprzez eksperymenty na skalę użyteczności. Jeśli ten kurs sprawi, że dołączysz do tych poszukiwań, pełnych wyzwań i zabawy, będziemy niezmiernie radzi.

Bibliografia