Skip to main content
IBM Quantum Platform

Combiner les options d'atténuation des erreurs avec la primitive Estimator

Estimation du temps d'exécution : 7 minutes sur un processeur Heron r2 (REMARQUE : il s'agit uniquement d'une estimation. (La durée d'exécution peut varier.)


Résultats d'apprentissage

Nous recommandons aux utilisateurs de se familiariser avec les sujets suivants avant de suivre ce tutoriel :

  • Les principes fondamentaux du découplage dynamique, de l'atténuation des erreurs de mesure, du « gate twirling » et de l'extrapolation sans bruit, tels que décrits dans ce guide.

Prérequis

À l'issue de ce tutoriel, les utilisateurs devraient avoir compris :

  • Comment les techniques d'atténuation des erreurs susmentionnées sont mises en œuvre de manière sélective au niveau matériel.
  • Comment se comparent-ils en termes de capacité à atténuer le bruit du matériel?

Arrière-plan

Ce tutoriel présente les options de suppression et d'atténuation des erreurs disponibles avec la primitive « Estimator » de l' IBM Quantum. Ce tutoriel explique comment implémenter chacune des méthodes suivantes séparément :

  • Découplage dynamique
  • Atténuation des erreurs de mesure
  • Tournoiement de la porte
  • Extrapolation à bruit nul (ZNE)

Il convient de noter qu'au lieu de mettre en œuvre ces techniques individuellement, il est possible de les mettre en œuvre à l'aide d'un niveau de résilience, qui resilience_level peut prendre les valeurs 0, 1 ou 2 :

  • 0 : Aucune mesure d'atténuation n'est mise en œuvre.
  • 1 : Une mesure visant à réduire les erreurs de mesure a été mise en place.
  • 2 : La rotation des portes, l'atténuation des erreurs de mesure et le ZNE sont mis en œuvre.

Dans ce tutoriel, vous allez monter un circuit et un observable, puis soumettre des tâches à l'aide de la primitive Estimator en utilisant différentes combinaisons de paramètres d'atténuation des erreurs. Vous représenterez ensuite les résultats graphiquement afin d'observer les effets des différents paramètres. La majeure partie du tutoriel utilise un circuit de 10 qubits pour faciliter la visualisation, et à la fin, vous étendrez le processus à 50 qubits.


Exigences

Avant de commencer ce guide, assurez-vous que les éléments suivants sont installés :

  • Qiskit SDK v2.1 ou plus tard, avec prise en charge de la visualisation
  • Qiskit Runtime v0.40 ou plus tard (pip install qiskit-ibm-runtime)

Configuration

import matplotlib.pyplot as plt
import numpy as np

from qiskit.circuit.library import efficient_su2, unitary_overlap
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Batch, EstimatorV2 as Estimator

Exemple de simulateur à petite échelle

Nous allons ignorer cette étape, car la gestion des erreurs d'exécution n'est pas prise en charge sur les simulateurs.


Exemple de matériel

Étape 1 : Mettre en correspondance les entrées classiques avec un problème quantique

Cette procédure suppose que le problème classique a déjà été transposé au niveau quantique. Commencez par construire un circuit et un observateur pour effectuer les mesures. Bien que les techniques utilisées ici s'appliquent à de nombreux types de circuits différents, pour plus de simplicité, ce guide utilise le efficient_su2 circuit inclus dans la bibliothèque de circuits Qiskit.

efficient_su2 Il s'agit d'un circuit quantique paramétré conçu pour être exécuté efficacement sur du matériel quantique doté d'une connectivité limitée entre les qubits, tout en restant suffisamment puissant pour résoudre des problèmes dans des domaines d'application tels que l'optimisation et la chimie. Il est construit en alternant des couches de portes à un qubit paramétrées avec une couche contenant un motif fixe de portes à deux qubits, pour un nombre de répétitions donné. La configuration des portes à deux qubits peut être définie par l'utilisateur. Ici, vous pouvez utiliser le schéma pairwise intégré, car il permet de réduire au minimum la profondeur du circuit en regroupant les portes à deux qubits de la manière la plus compacte possible. Ce schéma peut être mis en œuvre en utilisant uniquement une connectivité linéaire entre les qubits.

n_qubits = 10
reps = 1

circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)

circuit.decompose().draw("mpl", scale=0.7)

Output:

Output of the previous code cell

Comme observable, prenons l'opérateur d' ZZ de Pauli agissant sur le dernier qubit, ZIIZ I \cdots I. Notons que le fait que le dernier qubit corresponde au premier élément de cette chaîne est dû à l'utilisation par Qiskit de la notation « little-endian ».

# Z on the last qubit (index -1) with coefficient 1.0
observable = SparsePauliOp.from_sparse_list(
    [("Z", [-1], 1.0)], num_qubits=n_qubits
)

À ce stade, vous pouvez lancer votre circuit et mesurer la grandeur observable. Cependant, vous souhaitez également comparer le résultat obtenu par le dispositif quantique avec la réponse correcte, c'est-à-dire la valeur théorique de l'observable, si le circuit avait été exécuté sans erreur. Pour les petits circuits quantiques, il est possible de calculer cette valeur en simulant le circuit sur un ordinateur classique, mais cela n'est pas possible pour les circuits plus grands, destinés à une utilisation à grande échelle. Vous pouvez contourner ce problème grâce à la technique du « circuit miroir » (également appelée « compute-uncompute »), qui s'avère utile pour évaluer les performances des dispositifs quantiques.

Circuit miroir

Dans la technique du circuit miroir, vous concaténé le circuit avec son circuit inverse, qui est formé en inversant chaque porte du circuit dans l'ordre inverse. Le circuit résultant met en œuvre l'opérateur d'identité, qui peut trivialement être simulé. Comme la structure du circuit original est préservée dans le circuit miroir, l'exécution du circuit miroir donne toujours une idée de la manière dont le dispositif quantique fonctionnerait sur le circuit original.

La cellule de code suivante attribue des paramètres aléatoires à votre circuit, puis construit le circuit miroir à l'aide de la unitary_overlap classe. Avant de refléter le circuit, ajoutez-y une instruction de barrière afin d'empêcher le transpilateur de fusionner les deux parties du circuit situées de part et d'autre de la barrière, ce qui donnerait un circuit transpilé dépourvu de portes logiques.

# Generate random parameters
rng = np.random.default_rng(1234)
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)

# Assign the parameters to the circuit
assigned_circuit = circuit.assign_parameters(params)

# Add a barrier to prevent circuit optimization of mirrored operators
assigned_circuit.barrier()

# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)

mirror_circuit.decompose().draw("mpl", scale=0.7)

Output:

Output of the previous code cell

Étape 2 : Optimiser le problème pour l'exécution sur du matériel quantique

Vous devez optimiser votre circuit avant de l'exécuter sur du matériel. Ce processus comporte plusieurs étapes :

  • Choisissez une disposition de qubits qui fait correspondre les qubits virtuels de votre circuit aux qubits physiques du matériel.
  • Insérez des portes de permutation si nécessaire pour acheminer les interactions entre les qubits qui ne sont pas connectés.
  • Traduire les portes de votre circuit en instructions ISA (Instruction Set Architecture) qui peuvent être directement exécutées sur le matériel.
  • Effectuer des optimisations de circuit pour minimiser la profondeur du circuit et le nombre de portes.

Le transcompilateur intégré à Qiskit peut effectuer toutes ces étapes à votre place. Comme cet exemple utilise un circuit efficace sur le plan matériel, le transcompilateur devrait être capable de choisir une disposition des qubits qui ne nécessite pas l'insertion de portes d'échange pour acheminer les interactions.

Vous devez choisir le périphérique matériel à utiliser avant d'optimiser votre circuit. La cellule de code suivante recherche le dispositif le moins sollicité disposant d'au moins 127 qubits.

service = QiskitRuntimeService()
backend = service.least_busy(
    operational=True, simulator=False, min_num_qubits=127
)
print(backend)

Output:

<IBMBackend('ibm_fez')>

Vous pouvez transcompiler votre circuit vers le backend de votre choix en créant un gestionnaire de passes, puis en l'exécutant sur le circuit. Une façon simple de créer un gestionnaire de mots de passe consiste à utiliser la generate_preset_pass_manager fonction. Pour une explication plus détaillée de la transcompilation à l'aide des gestionnaires de passes, consultez la section « Transpile with pass managers ».

pass_manager = generate_preset_pass_manager(
    optimization_level=3, backend=backend, seed_transpiler=1234
)
isa_circuit = pass_manager.run(mirror_circuit)

isa_circuit.draw("mpl", idle_wires=False, scale=0.7, fold=-1)

Output:

Output of the previous code cell

Le circuit transpilé ne contient désormais plus que des instructions ISA. Toutes les portes ont été décomposées en portes de type « X\sqrt{X} », en rotations de type « RzR_z » et en portes CZ.

Le processus de transpilation a mis en correspondance les qubits virtuels du circuit avec les qubits physiques du matériel. Les informations relatives à la disposition du qubit sont stockées dans l'attribut layout du circuit transposé. L'observable a également été défini en termes de qubits virtuels, vous devez donc appliquer cette disposition à l'observable, ce que vous pouvez faire avec la méthode apply_layout de SparsePauliOp.

isa_observable = observable.apply_layout(isa_circuit.layout)

print("Original observable:")
print(observable)
print()
print("Observable with layout applied:")
print(isa_observable)

Output:

Original observable:
SparsePauliOp(['ZIIIIIIIII'],
              coeffs=[1.+0.j])

Observable with layout applied:
SparsePauliOp(['IIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
              coeffs=[1.+0.j])

Étape 3 : Exécutez à l'aide d' Qiskit primitives

Vous êtes maintenant prêt à faire fonctionner votre circuit en utilisant la primitive Estimator.

Vous allez ici soumettre cinq tâches distinctes, en commençant sans aucune option de suppression ou d'atténuation des erreurs, puis en activant successivement les différentes options de suppression et d'atténuation des erreurs disponibles dans le service de calcul d' IBM Quantum. Pour plus d'informations sur les options, consultez les pages suivantes :

Ces tâches pouvant s'exécuter indépendamment les unes des autres, vous pouvez utiliser le mode batch pour permettre à Quantum Compute d'optimiser le calendrier de leur exécution.

pub = (isa_circuit, isa_observable)

jobs = []

with Batch(backend=backend) as batch:
    estimator = Estimator(mode=batch)
    estimator.options.environment.job_tags = [
        "TUT_CEM_SS"
    ]  # add tag for this small scale job
    # Set number of shots
    estimator.options.default_shots = 100_000
    # Disable runtime compilation and error mitigation
    estimator.options.resilience_level = 0

    # Run job with no error mitigation
    job0 = estimator.run([pub])
    jobs.append(job0)

    # Add dynamical decoupling (DD)
    estimator.options.dynamical_decoupling.enable = True
    estimator.options.dynamical_decoupling.sequence_type = "XpXm"
    job1 = estimator.run([pub])
    jobs.append(job1)

    # Add readout error mitigation (DD + TREX)
    estimator.options.resilience.measure_mitigation = True
    job2 = estimator.run([pub])
    jobs.append(job2)

    # Add gate twirling (DD + TREX + Gate Twirling)
    estimator.options.twirling.enable_gates = True
    estimator.options.twirling.num_randomizations = "auto"
    job3 = estimator.run([pub])
    jobs.append(job3)

    # Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
    estimator.options.resilience.zne_mitigation = True
    estimator.options.resilience.zne.noise_factors = (1, 3, 5)
    estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
    job4 = estimator.run([pub])
    jobs.append(job4)

Étape 4 : Post-traitement et restitution du résultat dans le format classique souhaité

Enfin, vous pouvez analyser les données. Ici, vous récupérez les résultats du travail, vous en extrayez les valeurs attendues mesurées et vous tracez les valeurs, y compris les barres d'erreur d'un écart type.

# Retrieve the job results
results = [job.result() for job in jobs]

# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]

# Unpack the expectation values and standard errors
expectation_vals = np.array(
    [float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
    [float(pub_result.data.stds) for pub_result in pub_results]
)

# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
    range(len(labels)),
    expectation_vals,
    yerr=standard_errors,
    label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")

plt.show()

Output:

Output of the previous code cell

À cette petite échelle, il est difficile de voir l'effet de la plupart des techniques d'atténuation des erreurs, mais l'extrapolation à bruit nul apporte une amélioration notable. Il convient toutefois de noter que cette amélioration n'est pas gratuite, car le résultat ZNE présente également une barre d'erreur plus importante.


Exemple de matériel à grande échelle

Lors du développement d'une expérience, il est utile de commencer par un petit circuit afin de faciliter les visualisations et les simulations. Maintenant que vous avez développé et testé notre flux de travail sur un circuit à 10 qubits, vous pouvez le faire passer à 50 qubits. La cellule de code suivante répète toutes les étapes de cette procédure pas à pas, mais les applique désormais à un circuit de 50 qubits.

n_qubits = 50
reps = 1

# Construct circuit and observable
circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)
observable = SparsePauliOp.from_sparse_list(
    [("Z", [-1], 1.0)], num_qubits=n_qubits
)

# Assign parameters to circuit
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)
assigned_circuit = circuit.assign_parameters(params)
assigned_circuit.barrier()

# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)

# Transpile circuit and observable
isa_circuit = pass_manager.run(mirror_circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)

# Run jobs
pub = (isa_circuit, isa_observable)

jobs = []

with Batch(backend=backend) as batch:
    estimator = Estimator(mode=batch)
    estimator.options.environment.job_tags = [
        "TUT_CEM_LS"
    ]  # add tag for this large scale job
    # Set number of shots
    estimator.options.default_shots = 100_000
    # Disable runtime compilation and error mitigation
    estimator.options.resilience_level = 0

    # Run job with no error mitigation
    job0 = estimator.run([pub])
    jobs.append(job0)

    # Add dynamical decoupling (DD)
    estimator.options.dynamical_decoupling.enable = True
    estimator.options.dynamical_decoupling.sequence_type = "XpXm"
    job1 = estimator.run([pub])
    jobs.append(job1)

    # Add readout error mitigation (DD + TREX)
    estimator.options.resilience.measure_mitigation = True
    job2 = estimator.run([pub])
    jobs.append(job2)

    # Add gate twirling (DD + TREX + Gate Twirling)
    estimator.options.twirling.enable_gates = True
    estimator.options.twirling.num_randomizations = "auto"
    job3 = estimator.run([pub])
    jobs.append(job3)

    # Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
    estimator.options.resilience.zne_mitigation = True
    estimator.options.resilience.zne.noise_factors = (1, 3, 5)
    estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
    job4 = estimator.run([pub])
    jobs.append(job4)

# Retrieve the job results
results = [job.result() for job in jobs]

# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]

# Unpack the expectation values and standard errors
expectation_vals = np.array(
    [float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
    [float(pub_result.data.stds) for pub_result in pub_results]
)

# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
    range(len(labels)),
    expectation_vals,
    yerr=standard_errors,
    label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")

plt.show()

Output:

Output of the previous code cell

Lorsque vous comparez les résultats de 50 qubits avec les résultats de 10 qubits obtenus précédemment, vous pouvez noter les éléments suivants (vos résultats peuvent différer d'une série à l'autre) :

  • Toutes les expériences donnent des résultats plus proches de la valeur idéale et toutes les barres d'erreur sont plus petites.
  • L'ajout du découplage dynamique pourrait avoir nui aux performances par rapport au scénario sans mesure d'atténuation. Cela n'a rien d'étonnant, car le circuit est très dense. Le découplage dynamique est particulièrement utile lorsqu'il existe de longs intervalles dans le circuit, pendant lesquels les qubits restent inactifs sans qu'aucune porte ne leur soit appliquée. Lorsque ces intervalles font défaut, le découplage dynamique n'est pas efficace et peut même nuire aux performances en raison d'erreurs dans les impulsions de découplage dynamique elles-mêmes. Il se peut que le circuit de 10 qubits ait été trop petit pour nous permettre d'observer cet effet.
  • Avec une extrapolation sans bruit, le résultat est très proche de la valeur idéale. Cela démontre toute la puissance de ZNE.

Etapes suivantes

Recommandations

Si ce travail vous a paru intéressant, vous pourriez être intéressé par les ressources suivantes, qui traitent d'autres techniques d'atténuation et de suppression des erreurs non abordées dans ce tutoriel :

Cette page a-t-elle été utile ?
Signaler un bogue, une coquille ou proposer du contenu sur GitHub.