Zum Hauptinhalt springen

Utility-Scale-Experiment I

hinweis

Tamiya Onodera (5. Juli 2024)

PDF der Originalvorlesung herunterladen. Hinweis: Einige Code-Ausschnitte könnten veraltet sein, da es sich um statische Bilder handelt.

Die ungefähre QPU-Zeit für dieses Experiment beträgt 45 Sekunden.

1. Einführung in das Utility-Paper

In dieser Lektion führen wir einen Utility-Scale-Circuit aus, der im informell sogenannten „Utility-Paper" erscheint, das in Nature, Band 618, vom 15. Juni 2023, veröffentlicht wurde. Das Paper befasst sich mit der Zeitentwicklung des zweidimensionalen transversalen Ising-Modells. Konkret betrachten die Autoren die Spindynamik des Hamiltonians:

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

wobei J>0J > 0 die Kopplung nächster Nachbarspins mit i<ji < j und hh das globale transversale Feld ist. Die Spindynamik wird von einem Anfangszustand aus mithilfe der Trotter-Zerlegung erster Ordnung des Zeitentwicklungsoperators simuliert:

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}

wobei die Evolutionszeit TT in T/δtT / \delta t Trotter-Schritte unterteilt wird und RZiZj(θJ)\mathrm{R}_{Z_i Z_j}(\theta_J) sowie RXi(θh)\mathrm{R}_{X_i}(\theta_h) ZZZZ- bzw. XX-Rotationsgates sind.

Die Experimente wurden auf einem IBM Quantum® Eagle-Prozessor durchgeführt, einem 127-Qubit-Gerät mit Heavy-Hex-Konnektivität, wobei XX-Interaktionen auf alle Qubits und ZZZZ-Interaktionen auf alle Kanten der Coupling Map angewendet wurden. Da nicht alle ZZZZ-Interaktionen aufgrund von „Datenabhängigkeiten" gleichzeitig angewendet werden können, wird die Coupling Map eingefärbt, um sie in Schichten zu gruppieren. Interaktionen in einer Schicht erhalten dieselbe Farbe und können parallel angewendet werden.

Außerdem haben sich die Autoren der Einfachheit halber auf den Fall θJ=π/2\theta_J=-\pi /2 konzentriert.

Der wesentliche Beitrag des Papers besteht darin, Quantenschaltkreise in einem Umfang zu erstellen, der über die Zustandsvektorsimulation hinausgeht, sie auf verrauschten Quantencomputern auszuführen und dennoch zuverlässige Ergebnisse zu extrahieren. Damit haben sie den Nutzen verrauschter Quantencomputer demonstriert. Dazu nutzten sie Zero-Noise-Extrapolation (ZNE) mit probabilistischer Fehlerverstärkung (PEA) zur Fehlerminderung.

Seitdem nennen wir solche Experimente und Circuits „Utility-Scale".

1.1 Dein Ziel

Dein Ziel in dieser Lektion ist es, einen Utility-Scale-Circuit zu erstellen und ihn auf einem Eagle-Prozessor auszuführen. Es geht in diesem Notebook nicht darum, zuverlässige Ergebnisse zu extrahieren – teils weil PEA zum Zeitpunkt der Erstellung dieses Textes noch ein experimentelles Feature von Qiskit ist, teils weil die Anwendung von ZNE mit PEA erhebliche Zeit in Anspruch nehmen würde.

Konkret sollst du den Circuit erstellen und ausführen, der Figure 4b des Papers entspricht, und die „unmittelten" Punkte selbst darstellen. Wie du siehst, handelt es sich um einen Circuit mit 127 Qubits ×\times 60 Schichten (20 Trotter-Schritte) mit Z62\langle Z_{62} \rangle als Observable. image.png Klingt nach einer großen Aufgabe?   Keine Sorge. Die letzten drei Lektionen dieses Kurses bieten dafür Trittsteine. Zunächst demonstrieren wir ein kleineres Experiment: Wir erstellen einen 27-Qubit ×\times 6-Schichten-Circuit (2 Trotter-Schritte) mit Z13\langle Z_{13} \rangle als Observable und führen ihn auf einem Fake-Device aus.

Das war die Einführung. Auf ins Utility-Scale-Abenteuer!

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

2.1 RZZ(-π\pi / 2) konstruieren

Zunächst stellen wir fest, dass das RZZ-Gate im Allgemeinen zwei CXCX-Gates benötigt.

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

Wie oben erwähnt, konzentrieren wir uns für dieses Experiment auf das RZZ-Gate mit dem spezifischen Winkel -π\pi / 2. Wie im Paper gezeigt, lässt es sich mit nur einem CXCX-Gate realisieren.

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

Wir definieren ein Gate aus diesem Circuit für spätere Verwendung.

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

Machen wir eine erste Verwendung des neu definierten 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

Bevor wir dieses Gate weiter nutzen, überprüfen wir die logische Äquivalenz von qc1 (dem RZZ-Gate) für -pi/2 und unserem neu definierten rzz bzw. qc2-Gate:

from qiskit.quantum_info import Operator

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

op1.equiv(op2)
True

2.2 Coupling Map einfärben

Schauen wir uns an, wie wir die Coupling Map eines Backends einfärben. Das ist notwendig, um ZZZZ-Interaktionen in Schichten zu gruppieren.

Zunächst visualisieren wir die Coupling Map eines Backends. Beachte, dass die Coupling Maps für alle aktuellen IBM Quantum-Geräte Heavy-Hexagonal sind.

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

backend.coupling_map.draw()

Output of the previous code cell

Zum Einfärben der Coupling Map verwenden wir rustworkx, ein Python-Paket für die Arbeit mit Graphen und komplexen Netzwerken. Es bietet mehrere Einfärbungsalgorithmen, die alle heuristisch sind und daher keine minimale Einfärbung garantieren.

Da Heavy-Hex-Graphen bipartit sind, verwenden wir graph_bipartite_edge_color, das für diese Graphen eine minimale Einfärbung liefern sollte.

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

Heavy-Hexagonale Graphen sollten mit drei Farben eingefärbt werden. Überprüfen wir das für die obige Coupling Map.

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.

Genau richtig!

Zum Spaß färben wir die Coupling Map mit der ermittelten Einfärbung ein und nutzen dafür die Visualisierungsfunktionen von 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. Die Trotterisierte Zeitentwicklung eines 2D-Ising-Modells lösen.

Wir definieren eine Routine zum Erstellen des Circuits aus dem Utility-Paper für die Zeitentwicklung eines 2D-Ising-Modells. Die Routine nimmt drei Parameter entgegen: ein Backend, eine ganze Zahl für die Anzahl der Trotter-Schritte und einen Boolean zur Steuerung der Barrier-Einfügung.

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

Beachte, dass wir das Qubit-Mapping und das Routing für den erstellten Circuit bereits manuell durchgeführt haben. Wenn wir den Circuit später transpilieren, soll der Transpiler Qubit-Mapping und Routing daher nicht (nicht) erneut vornehmen. Wie du gleich sehen wirst, rufen wir ihn mit Optimierungsstufe 1 und der Layout-Methode „trivial" auf.

Als Nächstes definieren wir eine einfache Routine, um Informationen über den erstellten Circuit zur schnellen Überprüfung zu erhalten.

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()])}"""
)

Probieren wir diese Routinen aus. Du solltest einen Circuit mit 27 Qubits ×\times 15 Schichten (5 Trotter-Schritte) sehen. Da das Fake-Device 28 Kanten hat, sollte es 28*5 verschränkende Gates geben.

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

Output of the previous code cell

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. Die 27-Qubit-Version des Problems lösen.

Wir demonstrieren jetzt eine kleinere Version des Utility-Experiments. Wir erstellen einen 27-Qubit ×\times 6-Schichten-Circuit (2 Trotter-Schritte) mit Z13\langle Z_{13} \rangle als Observable und führen ihn sowohl auf dem AerSimulator als auch auf einem Fake-Device aus.

Dabei folgen wir natürlich unserem vierstufigen Workflow, dem „Qiskit-Muster", das aus Map, Optimize, Execute und Post-Process besteht. Konkret:

  • Klassische Eingaben einer Quantenberechnung zuordnen.
  • Circuits für die Quantenberechnung optimieren.
  • Circuits mit Primitives ausführen.
  • Ergebnisse nachverarbeiten und im klassischen Format zurückgeben.

Im Folgenden haben wir den Map-Schritt zum Erstellen eines Circuits für das kleinere Experiment. Dann folgen je ein Optimize- und Execute-Satz für den AerSimulator und ein weiterer für das Fake-Device. Abschließend haben wir den Post-Process-Schritt zur Darstellung der Ergebnisse.

4.1 Schritt 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 Schritte 2 und 3: Optimieren und Ausführen (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

Ein Nutzer hat die nächste Zelle auf einem MacBook Pro mit 2,3-GHz-Quad-Core-Intel-Core-i7-Prozessor und 32 GB 3LPDDR4X RAM unter macOS 14.5 ausgeführt. Die Wandzeit betrug 161 ms. Je nach Laptop kann es leicht variieren.

%%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 Schritte 2 und 3: Optimieren und Ausführen (Fake-Device)

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

Als derselbe Nutzer die nächste Zelle in derselben Umgebung ausführte, betrug die Wandzeit 2 min 19 s. Die Ausführung eines Circuits auf einem Fake-Device ruft eine verrauschte Simulation auf, die deutlich länger dauert als eine exakte Simulation. Wir empfehlen, keinen größeren Circuit (z. B. 27 Qubits ×\times 9 Schichten mit 3 Trotter-Schritten) auf einem Fake-Device auszuführen.

%%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 Schritt 4: Nachverarbeitung

Wir stellen die Ergebnisse aus der exakten und der verrauschten Simulation dar. Du siehst die gravierenden Auswirkungen des Rauschens auf 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()

Output of the previous code cell

5. Die 127-Qubit-Version des Problems lösen

Dein Ziel ist es, das Utility-Scale-Experiment wie zu Beginn beschrieben durchzuführen. Du wirst einen 127-Qubit ×\times 60-Schichten-Circuit (20 Trotter-Schritte) mit Z62\langle Z_{62} \rangle als Observable erstellen und ausführen. Wir empfehlen dir, das selbst zu versuchen und dabei den Code für die 27-Qubit-Version als Vorlage zu nutzen. Die Lösung ist jedoch hier angegeben.

Lösung:

5.1 Schritt 1: Map

# 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 Schritte 2 und 3: Optimieren und Ausführen

Hinweis: Die Coupling Map des Eagle-Prozessors hat 144 Kanten.

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

Wir stellen die Werte für die „mittelten" Punkte aus Figure 4b des Utility-Papers bereit. Stelle diese zusammen mit deinen eigenen Ergebnissen dar.

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

Ähneln deine Ergebnisse den „unmittelten" Punkten aus Figure 4b?   Sie könnten sehr unterschiedlich sein, je nach Gerät und dessen Zustand zum Zeitpunkt des Experiments. Mach dir über die Ergebnisse selbst keine Sorgen. Was wir prüfen, ist, ob du die Programmierung korrekt durchgeführt hast. Wenn ja, herzlichen Glückwunsch – du hast die Startlinie des Utility-Zeitalters erreicht.

Wie im Utility-Paper beschrieben, haben Wissenschaftlerinnen und Wissenschaftler auf der ganzen Welt großen Einfallsreichtum aufgewendet, um selbst in Anwesenheit von Rauschen aussagekräftige Ergebnisse zu extrahieren. Das übergeordnete Ziel dieser kollektiven Bemühungen ist der Quantenvorteil: ein Zustand, in dem Quantencomputer bestimmte industrierelevante Probleme schneller, mit höherer Genauigkeit oder kostengünstiger lösen können als klassische Computer. Es wird sich dabei voraussichtlich nicht um ein einzelnes Ereignis handeln, sondern um eine Ära, in der die klassische Reproduktion von Quantenergebnissen zunehmend länger dauert, bis dieser Vorsprung irgendwann entscheidend wichtig wird. Eines ist klar: Wir erreichen den Quantenvorteil nur durch Utility-Scale-Experimente. Wenn dieser Kurs dazu führt, dass du dich dem Weg dorthin anschließt – voller Herausforderungen und Spaß –, würden wir uns sehr freuen.

Referenz