Expérience à l'échelle utilitaire I
Tamiya Onodera (5 juillet 2024)
Télécharger le PDF du cours original. Note que certains extraits de code peuvent être obsolètes car ce sont des images statiques.
Le temps QPU approximatif pour exécuter cette expérience est de 45 secondes.
1. Introduction au utility paper
Dans cette leçon, nous exécutons un circuit à l'échelle utilitaire qui figure dans ce qu'on appelle communément « le utility paper », publié dans Nature Vol 618, 15 juin 2023. L'article traite de l'évolution temporelle du modèle d'Ising 2D en champ transverse. En particulier, ils étudient la dynamique temporelle du Hamiltonien,
dans lequel est le couplage entre spins voisins avec et est le champ transverse global. Ils simulent la dynamique de spin à partir d'un état initial au moyen d'une décomposition de Trotter au premier ordre de l'opérateur d'évolution temporelle,
dans lequel le temps d'évolution est discrétisé en pas de Trotter, et et sont respectivement les portes de rotation et .
Ils ont mené des expériences sur un processeur IBM Quantum® Eagle, un dispositif de 127 qubits avec une connectivité heavy-hex, en appliquant les interactions à tous les qubits et les interactions à toutes les arêtes de la carte de couplage. Remarque : toutes les interactions ne peuvent pas être appliquées simultanément en raison de « dépendances de données ». Par conséquent, ils colorient la carte de couplage pour les regrouper en couches. Celles appartenant à une même couche reçoivent la même couleur et peuvent être appliquées en parallèle.
De plus, par souci de simplicité expérimentale, ils se sont concentrés sur le cas .
La contribution originale de l'article est qu'ils ont construit des circuits quantiques à une échelle dépassant la simulation par vecteur d'état, les ont exécutés sur des ordinateurs quantiques bruités et ont réussi à en extraire des résultats fiables. Autrement dit, ils ont démontré l'utilité des ordinateurs quantiques bruités. Pour ce faire, ils ont appliqué l'extrapolation du bruit zéro (ZNE) avec l'amplification probabiliste des erreurs (PEA) afin d'atténuer les erreurs des dispositifs bruités.
Depuis lors, on désigne ce type d'expériences et de circuits par le terme « à l'échelle utilitaire ».
1.1 Ton objectif
Ton objectif dans cette leçon est de construire un circuit à l'échelle utilitaire et de l'exécuter sur un processeur Eagle. Il n'entre pas dans le cadre de ce notebook d'extraire des résultats fiables, en partie parce que PEA est une fonctionnalité expérimentale de Qiskit au moment de la rédaction, et en partie parce qu'appliquer ZNE avec PEA demande un temps considérable.
Concrètement, tu dois construire et exécuter le circuit correspondant à la Figure 4b de l'article, et tracer toi-même les points « non atténués ». Comme tu peux le constater, il s'agit d'un circuit de 127 qubits 60 couches (20 pas de Trotter) avec comme observable.
Ça semble impressionnant ? Ne t'inquiète pas. Les trois dernières leçons de ce cours fournissent des étapes intermédiaires. Pour commencer, nous allons démontrer une expérience à plus petite échelle : construire et exécuter sur un dispositif factice un circuit de 27 qubits 6 couches (2 pas de Trotter) avec comme observable.
C'est tout pour l'introduction. Partons à l'aventure à l'échelle utilitaire !
# 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. Préparation
2.1 Construire RZZ(- / 2)
Pour commencer, observe que la porte RZZ en général nécessite deux portes .
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")
Comme mentionné plus haut, nous nous concentrons sur la porte RZZ avec un angle spécifique, - / 2, pour cette expérience. Comme indiqué dans l'article, elle peut être réalisée avec une seule porte .
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")
Nous définissons une porte à partir de ce circuit pour référence future.
rzz = qc2.to_gate(label="RZZ")
Faisons un usage rapide de la porte rzz nouvellement définie.
qc3 = QuantumCircuit(3)
qc3.append(rzz, [0, 1])
qc3.append(rzz, [0, 2])
display(qc3.draw("mpl"))
# display(qc.decompose(reps=1).draw("mpl"))
Avant d'aller plus loin, vérifions l'équivalence logique de qc1 (la porte RZZ) pour -pi/2 et notre porte rzz ou qc2 nouvellement définie :
from qiskit.quantum_info import Operator
op1 = Operator(qc1.assign_parameters([-np.pi / 2]))
op2 = Operator(qc2)
op1.equiv(op2)
True
2.2 Colorier la carte de couplage
Voyons comment colorier la carte de couplage d'un backend. Cela est nécessaire pour regrouper les interactions en couches.
Pour commencer, visualisons la carte de couplage d'un backend. Note que les cartes de couplage sont de type heavy-hexagonal pour tous les dispositifs IBM Quantum actuels.
backend = service.least_busy(operational=True, simulator=False)
backend.coupling_map.draw()

Pour colorier une carte de couplage, nous utilisons rustworkx, un package Python permettant de travailler avec des graphes et des réseaux complexes. Il fournit plusieurs algorithmes de coloration, qui sont tous heuristiques et ne garantissent donc pas de trouver une coloration minimale.
Cela dit, comme les graphes heavy-hex sont bipartis, nous utilisons graph_bipartite_edge_color, qui devrait trouver une coloration minimale pour ces graphes.
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
Les graphes heavy-hexagonaux devraient être colorés en trois couleurs. Vérifions cela pour la carte de couplage ci-dessus.
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.
Effectivement !
Pour le plaisir, colorions la carte de couplage selon la coloration obtenue, en utilisant la fonctionnalité de visualisation de 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"]}
)

3. Résoudre l'évolution temporelle de Trotter d'un modèle d'Ising 2D
Définissons une routine pour construire le circuit du utility paper pour l'évolution temporelle d'un modèle d'Ising 2D. La routine prend trois paramètres : un backend, un entier indiquant le nombre de pas de Trotter, et un booléen contrôlant l'insertion de barrières.
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
Note que nous avons déjà effectué manuellement le mappage et le routage des qubits pour le circuit construit. Ainsi, lors de la transpilation du circuit plus tard, nous ne demandons pas (ne devons pas demander) au transpiler de faire le mappage et le routage des qubits. Comme tu le verras bientôt, nous l'invoquons avec le niveau d'optimisation 1 et la méthode de disposition « trivial ».
Ensuite, définissons une routine simple pour obtenir des informations sur le circuit construit, afin d'effectuer une vérification rapide.
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()])}"""
)
Exerçons-nous avec ces routines. Tu devrais voir un circuit de 27 qubits 15 couches (5 pas de Trotter). Comme le dispositif factice a 28 arêtes, il devrait y avoir 28*5 portes d'intrication.
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))

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. Résoudre la version 27 qubits du problème
Nous allons maintenant démontrer une version à plus petite échelle de l'expérience utilitaire. Nous construisons un circuit de 27 qubits 6 couches (2 pas de Trotter) avec comme observable, et nous l'exécutons à la fois sur AerSimulator et sur un dispositif factice.
Bien entendu, nous suivons notre flux de travail en quatre étapes, le « Qiskit pattern », qui comprend : Map, Optimize, Execute et Post-Process. Plus concrètement :
- Mapper les entrées classiques vers un calcul quantique.
- Optimiser les circuits pour le calcul quantique.
- Exécuter les circuits en utilisant les primitives.
- Post-traiter et retourner les résultats au format classique.
Dans ce qui suit, nous avons l'étape Map pour créer un circuit pour une expérience à plus petite échelle. Nous avons ensuite un ensemble Optimize et Execute pour AerSimulator, et un autre pour un dispositif factice. Enfin, nous avons l'étape Post-Process pour tracer les résultats.
4.1 Étape 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 Étapes 2 et 3 : Optimize et Execute (Simulateur)
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
Un utilisateur a exécuté la cellule suivante sur un MacBook Pro avec un processeur Intel Core i7 quadricœur à 2,3 GHz et 32 Go de RAM 3LPDDR4X, sous macOS 14.5. Le temps d'exécution était de 161 ms. Chaque ordinateur portable sera légèrement différent.
%%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 Étapes 2 et 3 : Optimize et Execute (dispositif factice)
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
Quand le même utilisateur a exécuté la cellule suivante dans le même environnement, cela a pris 2 min 19 s en temps réel. L'exécution d'un circuit sur un dispositif factice invoque une simulation bruitée qui prend beaucoup plus de temps qu'une simulation exacte. Nous te recommandons de ne pas exécuter un circuit plus grand (par exemple, 27 qubits 9 couches avec 3 pas de Trotter) sur un dispositif factice.
%%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 Étape 4 : Post-process
Nous traçons les résultats des simulations exacte et bruitée. Tu observes les effets sévères du bruit sur 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()
5. Résoudre la version 127 qubits du problème
Ton objectif est d'exécuter l'expérience à l'échelle utilitaire mentionnée au début. Tu vas créer et exécuter un circuit de 127 qubits et 60 couches (20 pas de Trotter) avec comme observable. Nous te recommandons d'essayer de le faire toi-même en t'appuyant sur le code de la version 27 qubits lorsque c'est pertinent. La solution est néanmoins fournie ici.
Solution :
5.1 Étape 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 Étapes 2 et 3 : Optimize et Execute
Note que la carte de couplage du processeur Eagle possède 144 arêtes.
# 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 Post-process
Nous fournissons les valeurs des points « atténués » de la Figure 4b du utility paper. Trace-les avec tes propres résultats.
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()
Tes résultats ressemblent-ils aux points « non atténués » de la Figure 4b ? Ils pourraient être très différents selon le dispositif et son état au moment de l'expérience. Ne t'inquiète pas des résultats eux-mêmes. Ce que nous vérifierons, c'est si tu as fait le codage correctement. Si c'est le cas, félicitations, tu as atteint la ligne de départ de l'ère utilitaire.
Comme dans le utility paper, des scientifiques du monde entier ont fait preuve d'une ingéniosité considérable pour extraire des résultats significatifs malgré la présence de bruit. L'objectif final de cet effort collectif est l'avantage quantique : un état dans lequel les ordinateurs quantiques peuvent résoudre certains problèmes utiles à l'industrie plus rapidement, avec une meilleure fidélité ou à moindre coût que les ordinateurs classiques. Ce ne sera probablement pas un événement unique, mais plutôt une ère au cours de laquelle la reproduction classique des résultats quantiques prendra progressivement plus de temps, jusqu'au point où le délai d'avance quantique deviendra crucial. Une chose est claire concernant l'avantage quantique : nous n'y parviendrons que grâce aux expériences à l'échelle utilitaire. Si ce cours te conduit à rejoindre cette quête, riche en défis et en plaisir, nous en serions plus qu'heureux.
Référence
- Kim, Y., Eddins, A., Anand, S. et al. Evidence for the utility of quantum computing before fault tolerance. Nature 618, 500–505 (2023). https://doi.org/10.1038/s41586-023-06096-3