Skip to main content
IBM Quantum Platform

Améliorer les valeurs attendues grâce à l'absorption du bruit propagé (PNA)

Durée d'exécution estimée : 10 minutes sur un processeur Heron (REMARQUE : il s'agit uniquement d'une estimation. (Votre temps d'exécution peut varier.)


Acquis d'apprentissage

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

  • Qu'est-ce que l'absorption du bruit propagé (PNA) et comment atténue-t-elle le bruit des portes à deux qubits en absorbant les canaux de bruit inverses appris dans l'observable mesurée?
  • Comment utiliser samplomatic pour encadrer et annoter les couches d'un circuit en vue d'une rotation, d'un changement de base et d'une injection de bruit
  • Comment apprendre le bruit de couche avec [NoiseLearnerV3]( /docs/guides/directed-execution-model#noiselearnerv3 ) et le propager dans un observable d'atténuation du bruit à l'aide de qiskit-addon-pna
  • Comment échantillonner des circuits aléatoires à l'aide des classes Executor et QuantumProgram dans qiskit-ibm-runtime, et combiner PNA avec TREX et la post-sélection

Prérequis

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


Arrière-plan

Dans ce tutoriel, nous montrons comment tirer parti des outils avancés d'atténuation des erreurs de Qiskit pour améliorer l'estimation de la valeur attendue dans le cadre d'expériences quantiques soumises à du bruit.

Qu'est-ce que l'absorption acoustique des bruits propagés (PNA)?

L'absorption du bruit propagé est une technique visant à atténuer les erreurs de porte en faisant passer l'observable par le canal de bruit inverse affectant les portes à deux qubits, ce qui permet d'obtenir une observable atténuant le bruit.

Nous pouvons recourir à l'atténuation des erreurs quantiques pour extraire des valeurs d'espérance utiles à partir d'un matériel quantique sujet au bruit, sans pour autant nécessiter une tolérance totale aux pannes. La technique PNA vise spécifiquement à absorber les effets du bruit au sein même de la grandeur observable, plutôt que de modifier le fonctionnement du circuit.

Chaque porte soumise au bruit dans un circuit quantique peut être modélisée comme une porte idéale suivie d'un canal de bruit. Le PNA apprend ou caractérise ces canaux de bruit et définit leurs inverses. Au lieu d'intégrer les opérations inverses dans l'exécution matérielle (ce qui est généralement irréalisable), le PNA propage les canaux de bruit inverses vers l'avant à travers le circuit et les applique à l'observable. Ce processus transforme l' OO observable en un nouvel opérateur O~\tilde{O}, de telle sorte que la mesure de O~\tilde{O} sur le circuit sujet au bruit donne la même valeur d'espérance que la mesure de OO sur un circuit idéal, exempt de bruit.

On peut résumer cela ainsi :

  1. Modélisez chaque porte bruyante uiu_i comme une opération idéale suivie d'un canal de bruit Λi\Lambda_i.
  2. Déterminez ou estimez chaque « Λi\Lambda_i » à l'aide d'outils de caractérisation du bruit.
  3. Définir et propager les cartes de bruit inverses Λi1\Lambda_i^{-1} en aval du circuit à l'aide des techniques de transfert de Pauli.
  4. Intégrer ces inverses dans l'observable, ce qui donne un opérateur à bruit atténué O~\tilde{O}.

Lorsque le bruit est décrit comme un canal de Pauli (ou, plus généralement, comme un canal de Pauli-Lindblad clairsemé), cette propagation peut être effectuée efficacement à l'aide de la propagation de Pauli. La propagation de Pauli offre un cadre permettant d'estimer comment les canaux de bruit inverses se transforment lorsqu'ils traversent des couches d'opérations de Clifford et non-Clifford, tout en maîtrisant la complexité de calcul.

En transférant la compensation vers le domaine observable, la PNA évite l'importante charge d'échantillonnage qui résulterait autrement de l'insertion d'opérations de correction physique dans le circuit. Au lieu de cela, le circuit bruyant d'origine est exécuté, tandis que l'observable est transformée en un nouvel opérateur O~\tilde{O} dont la valeur attendue annule les effets du bruit.

Le processus PNA

Ce processus peut être appréhendé à travers les étapes conceptuelles suivantes. Le premier schéma illustre une expérience standard en présence de bruit.

Expérience bruyante

Si nous apprenons le modèle de bruit, nous pouvons appliquer son inverse et éliminer le bruit.

Expérience avec atténuation du bruit

Au lieu de mettre en œuvre le canal de bruit inverse en l'échantillonnant sur le QPU, comme dans la technique d'annulation probabiliste des erreurs (PEC), nous l'appliquons de manière classique à l'observable mesurée en utilisant la propagation de Pauli. L'observable ainsi obtenue atténue efficacement le bruit de la porte appris lors de la mesure.

Présentation de la PNA

Atténuation modulaire des erreurs avec Samplomatic et Executor

L'approche PNA s'appuie sur une architecture modulaire de réduction des erreurs intégrée à Qiskit. Cette architecture utilise la samplomatic bibliothèque ainsi que les QuantumProgram classes et Executor (ajoutées à Qiskit Runtime dans qiskit-ibm-runtimev0.47.0 ) afin de rendre des techniques telles que l'absorption du bruit propagé et la rotation de Pauli modulables et réutilisables dans différentes expériences.

Plutôt que d'intégrer la logique d'atténuation au sein même de la définition du circuit, l'atténuation est exprimée de manière déclarative par le biais samplomatic d'annotations et gérée par programmation via Executor, qui contrôle la manière dont les circuits aléatoires sont générés, exécutés et post-traités.

Dans ce tutoriel, nous mettons en œuvre un modèle Qiskit afin de montrer comment le PNA peut propager des canaux de bruit de Pauli inversés et modifier l'observable en conséquence, dans le but d'améliorer l'estimation des valeurs attendues sur des QPU soumises à du bruit.

Présentation du flux de travail

  • Étape 1 : Transposition au problème quantique
    • Construire un modèle d'Ising « kické » de type Trotter avec symétrie de réflexion et définir une observable cible.
  • Étape 2 : Caractériser et propager le bruit
    • Utilisez samplomatic pour identifier et annoter les couches à deux qubits et les mesures spécifiques présentes dans le circuit.
    • Découvrez le bruit affectant chaque couche distincte à l'aide de NoiseLearnerV3.
    • Associez chaque InjectNoise annotation au modèle de bruit appris correspondant.
    • Utilisez qiskit-addon-pna pour propager les canaux de bruit inverses vers l'avant à travers le circuit et les intégrer dans l'observable cible.
  • Étape 3 : Réaliser des expériences quantiques
    • Définissez un QuantumProgram pour spécifier un échantillonnage aléatoire via samplex et exécutez les expériences sur le backend à l'aide de Executor.
  • Étape 4 : Reconstitution et analyse des résultats
    • Comparer les stratégies d'atténuation (PNA, PNA+TREX, PNA+PS, PNA+PS+TREX) et visualiser l'amélioration par rapport aux résultats sans mesures d'atténuation.

Exigences

Avant de commencer ce tutoriel, assurez-vous d'avoir installé les éléments suivants :

  • Qiskit SDK v2.2 ou version ultérieure, avec prise en charge de la visualisation
  • Qiskit Runtime v0.47 ou version ultérieure (pip install qiskit-ibm-runtime)
  • Samplomatic v0.13 ou version ultérieure (pip install samplomatic)
  • Module complémentaire PNA Qiskit (pip install qiskit-addon-pna)
  • Utilitaires du module complémentaire Qiskit (pip install qiskit-addon-utils)

Configuration

from qiskit import QuantumCircuit
from qiskit.quantum_info import Pauli, SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager, PassManager
from qiskit_ibm_runtime import (
    QiskitRuntimeService,
    QuantumProgram,
    Executor,
    NoiseLearnerV3,
)
from qiskit_addon_utils.exp_vals.measurement_bases import (
    get_measurement_bases,
)
from qiskit_addon_utils.exp_vals.expectation_values import (
    executor_expectation_values,
)
from qiskit_addon_utils.noise_management import trex_factors
from qiskit_addon_utils.noise_management.post_selection import PostSelector
from qiskit_addon_utils.noise_management.post_selection.transpiler.passes import (
    AddPostSelectionMeasures,
    AddSpectatorMeasures,
)
from qiskit_addon_pna import generate_noise_mitigating_observable
import samplomatic
from samplomatic.transpiler import generate_boxing_pass_manager
from samplomatic.annotations import InjectNoise
from samplomatic.utils import get_annotation, find_unique_box_instructions
import numpy as np
import matplotlib.pyplot as plt


# Selects a connected chain of low-error qubits on the target backend.
# The mirrored kicked-Ising circuit is a 1D chain, so we only need a line
# of connected physical qubits; the helper walks the backend's coupling map
# and grows a chain along the lowest-error two-qubit edges, so it works for
# any backend rather than relying on a hardcoded layout.
def find_qubit_chain(backend, length):
    """Find a connected chain of ``length`` physical qubits on ``backend``.

    The chain is grown greedily along the lowest-error two-qubit edges, so it
    favors better-performing qubits. Because the mirrored kicked-Ising circuit
    is a 1D chain, a connected line is all we need.
    """
    target = backend.target

    # Identify the native two-qubit gate and build a per-edge error lookup.
    two_qubit_gate = next(
        name
        for name in target.operation_names
        if target[name]
        and all(q is not None and len(q) == 2 for q in target[name])
    )
    edge_error = {
        frozenset(qargs): (
            1.0 if props is None or props.error is None else props.error
        )
        for qargs, props in target[two_qubit_gate].items()
    }

    graph = backend.coupling_map.graph.to_undirected(multigraph=False)
    neighbors = {n: list(graph.neighbors(n)) for n in graph.node_indices()}

    def first_chain_from(start):
        path, visited = [start], {start}

        def grow():
            if len(path) == length:
                return True
            node = path[-1]
            order = sorted(
                neighbors[node],
                key=lambda m: edge_error.get(frozenset((node, m)), 1.0),
            )
            for nxt in order:
                if nxt not in visited:
                    visited.add(nxt)
                    path.append(nxt)
                    if grow():
                        return True
                    path.pop()
                    visited.remove(nxt)
            return False

        return path if grow() else None

    def chain_cost(path):
        return sum(
            edge_error.get(frozenset((path[i], path[i + 1])), 1.0)
            for i in range(len(path) - 1)
        )

    # Try low-degree qubits first (the natural ends of long chains) and keep
    # the lowest-error chain found.
    best_path, best_cost = None, float("inf")
    for start in sorted(neighbors, key=lambda n: len(neighbors[n])):
        chain = first_chain_from(start)
        if chain is not None and (cost := chain_cost(chain)) < best_cost:
            best_path, best_cost = chain, cost

    if best_path is None:
        raise ValueError(
            f"Could not find a connected chain of {length} qubits "
            f"on '{backend.name}'."
        )
    return best_path

Exemple de simulateur à petite échelle

Le PNA atténue le bruit physique des portes à deux qubits d'un processeur quantique spécifique. Le déroulement du processus repose sur deux services matériels qui n'ont pas d'équivalent significatif dans un simulateur idéal :

  • NoiseLearnerV3 caractérise expérimentalement le canal de bruit de Pauli-Lindblad clairsemé associé à chaque couche unique à deux qubits du circuit transpilé. Sur un simulateur sans bruit, il n'y a pas de bruit à apprendre, et le canal inverse propagé serait le canal d'identité.
  • La fonction « Executor primitive » échantillonne les circuits aléatoires et entremêlés générés par samplomatic sur un backend.

En principe, on pourrait utiliser un modèle de bruit synthétique à la place. Par exemple, vous pourriez joindre PauliLindbladError des instructions à l'aide de Qiskit Aer et transmettre directement le circuit bruité obtenu à generate_noise_mitigating_observable. Cependant, cela ne fait que valider la comptabilité classique par rapport au bruit que vous avez vous-même introduit, et passe à côté de l'intérêt de cette technique. C'est pourquoi nous ne nous appuierons pas sur l'exemple du simulateur et présenterons le flux de travail PNA complet directement sur le matériel, en détaillant ci-dessous chaque étape du modèle Qiskit.


Exemple de matériel à grande échelle

Nous exécutons désormais le flux de travail PNA complet sur un modèle d'Ising à 30 sites avec effet « kick », sur du matériel d IBM Quantum®, en suivant les quatre étapes d'un modèle Qiskit.

Étape 1 : Transposition en un problème quantique

Générer le circuit de Trotter symétrique et l'observable

Dans le cadre de cette expérience, nous étudierons la dynamique temporelle d’un modèle d’Ising à 30 sites avec effet de « kick » sur une chaîne de spins de type « 1D ». L'hamiltonien considéré est le suivant :

H=Ji,jZiZj+hiXiH = -J\sum\limits_{\langle i,j \rangle} Z_iZ_j + h\sum\limits_iX_i,

J>0J>0 décrit le couplage entre les spins voisins les plus proches, i<ji<j, et le champ transversal global, hh, est fixé à π8\frac{\pi}{8}. Plus l' hh est éloignée d'un angle de Clifford (c'est-à-dire θ=nπ2,nZ\theta=n\frac{\pi}{2}, n \in \mathbb{Z} ), plus il devient difficile de propager les générateurs d'anti-bruit à travers le circuit.

Pour le choix de la grandeur observable, nous considérerons la magnétisation moyenne par site, 1Ni=1Nzi\frac{1}{N} \sum_{i=1}^{N} \langle z_i \rangle, où NN est le nombre de sites.

num_qubits = 30
num_trotter_steps = 10
rx_angle = np.pi / 8

# Avg single-site magnetization
id_pauli = Pauli("I" * num_qubits)
observable = (
    SparsePauliOp([id_pauli.dot(Pauli("Z"), [i]) for i in range(num_qubits)])
    / num_qubits
)

# Implement Trotterized kicked-Ising model
circuit = QuantumCircuit(num_qubits)
for _step in range(num_trotter_steps):
    circuit.rx(rx_angle, range(num_qubits))
    for first_qubit in (1, 2):
        for idx in range(first_qubit, num_qubits, 2):
            # equivalent to Rzz(-pi/2):
            circuit.sdg([idx - 1, idx])
            circuit.cz(idx - 1, idx)
# Append the inverse circuit to complete the mirroring
circuit.compose(circuit.inverse(), inplace=True)
circuit.measure_active()
circuit.draw("mpl", fold=-1)

Output:

Output of the previous code cell

Étape 2 : Optimiser le problème en vue de son exécution sur le matériel

L'étape suivante consiste à préparer notre circuit Trotter en miroir en vue de son exécution sur du matériel « IBM Quantum ». L'exécution sur un QPU ne se limite pas à la simple construction du circuit abstrait; il faut en effet l'optimiser afin que :

  1. Il respecte le jeu de portes et la connectivité natifs du backend. La transpilation permet de convertir le circuit logique en un circuit ISA pris en charge par le backend cible. Cela garantit que chaque interaction entre une porte et un qubit est physiquement réalisable.

  2. Nous pouvons caractériser le bruit au niveau des couches du circuit. La méthode PNA repose sur l'apprentissage et la propagation de canaux de bruit inverses. Pour y parvenir efficacement, nous décomposons le circuit transpilé en couches « encapsulées » distinctes de deux qubits. Ces cases nous permettent d'associer à chaque couche du circuit son propre modèle de bruit appris.

  3. Nous pouvons intégrer des modèles de bruit réalistes dans PNA. Une fois le circuit encapsulé, nous utilisons le NoiseLearnerV3 service pour déterminer de manière expérimentale les canaux de bruit de Pauli affectant chaque couche unique à deux qubits. Ces modèles appris sont ensuite réintégrés dans le circuit via les annotations de Samplomatic.

Au final, cette étape comble le fossé entre un circuit « miroir » idéalisé et un circuit prêt à être implémenté sur matériel, intégrant des modèles de bruit appris. Avec cette configuration, le PNA peut propager des canaux de bruit inversés à travers le circuit et ajuster l'observable en conséquence.

Se connecter au backend et effectuer la compilation vers un circuit ISA

Nous commençons par initialiser le service de calcul d' IBM Quantum, puis nous sélectionnons un backend. Connectez-vous avec votre propre compte en suivant les instructions pour enregistrer vos identifiants; ceux-ci QiskitRuntimeService() seront ensuite récupérés automatiquement.

# Initialize IBM Quantum Compute Service using your saved credentials
service = QiskitRuntimeService()

backend = service.least_busy(
    operational=True, simulator=False, min_num_qubits=num_qubits
)
# Re-fetch with fractional gates enabled (least_busy does not forward this)
# Fractional gates are enabled so the non-Clifford Rx rotations are supported natively.
backend = service.backend(backend.name, use_fractional_gates=True)
print(f"Selected backend: {backend.name}")

Output:

Selected backend: ibm_fez

Ensuite, nous choisissons une chaîne connectée de qubits dans le backend et nous y transposons le circuit.

À l'aide de la find_qubit_chain fonction d'aide définie dans la section « Configuration », nous sélectionnons une ligne de num_qubits qubits physiques connectés. Nous procédons ensuite à la transcompilation à l'aide de optimization_level=0 et fixons cette chaîne en tant que initial_layout, ce qui préserve exactement la structure en couches de portes à deux qubits du circuit en miroir. C'est sur cette structure que reposent les étapes de « boxing » et d'apprentissage du bruit; il faut donc éviter ici un niveau d'optimisation plus élevé (qui annulerait les portes en miroir).

# Find a connected, low-error chain of qubits on the chosen backend
layout = find_qubit_chain(backend, num_qubits)

# Transpile the circuit for the target backend, pinning the chain as the layout.
# optimization_level=0 preserves the mirrored two-qubit-gate layers that the
# boxing and noise-learning steps rely on.
pm = generate_preset_pass_manager(
    backend=backend, optimization_level=0, initial_layout=layout
)
isa_circuit = pm.run(circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)
isa_circuit.draw("mpl", fold=-1)

Output:

Output of the previous code cell

Faites tourner les couches de portes à deux qubits et les mesures, puis identifiez les couches uniques

Nous utilisons samplomatic pour encadrer le circuit et identifier des couches uniques de deux qubits. Une « boîte » est une structure qui regroupe des instructions afin que des intentions ou des annotations spécifiques puissent ensuite être appliquées de manière uniforme à toutes les portes contenues dans cette même boîte.

generate_boxing_pass_managerIci, nous appelons cette méthode, qui va au-delà de la simple identification de couches à deux qubits. Il remplit plusieurs fonctions essentielles :

  • Regroupe toutes les couches à deux qubits du circuit,
  • Applique les Twirl annotations et ChangeBasis à ces calques,
  • regroupe les opérations de mesure dans des encadrés distincts, et
  • Applique l'annotation InjectNoise à chaque couche de deux qubits.

Ces annotations définissent la manière dont le bruit, les changements de base et les rotations sont gérés tout au long du circuit. Ils définissent également la structure qui sera ensuite utilisée pour l'apprentissage et l'atténuation du bruit.

Les principales options de configuration sont les suivantes :

  • enable_gates/ enable_measures: True: Mettre entre crochets toutes les couches de portes à deux qubits et les mesures terminales. Les portes à un seul qubit sont représentées selon la notation « left-dressed » à l'intérieur des encadrés.
  • measure_annotations: all: Indiquez Twirl les annotations et ChangeBasis sur le cadre de mesure.
  • twirling_strategy: active: Effectuer une rotation sur tous les qubits actifs dans chaque case contenant des portes d'intrication.
  • Twirl``inject_noise_targets: gates: Ajouter InjectNoise des annotations à toutes les cases annotées contenant des portes d'intrication.
  • inject_noise_strategy: uniform_modification: Régler le niveau de toutes les couches de bruit de manière équivalente sur l'ensemble du circuit.
# Box up circuit with Twirl and InjectNoise annotations
pm = generate_boxing_pass_manager(
    enable_gates=True,
    enable_measures=True,
    measure_annotations="all",
    twirling_strategy="active",
    inject_noise_targets="gates",
    inject_noise_strategy="uniform_modification",
)
boxed_circuit = pm.run(isa_circuit)
draw_circ = QuantumCircuit(boxed_circuit.num_qubits)
draw_circ.append(boxed_circuit.data[0], qargs=boxed_circuit.data[0].qubits)
draw_circ.append(boxed_circuit.data[1], qargs=boxed_circuit.data[1].qubits)
draw_circ.draw("mpl", fold=-1, scale=0.3, idle_wires=False)

Output:

Output of the previous code cell

Générez le circuit modèle et samplex, qui définissent la manière dont le circuit sera échantillonné.

Nous ajoutons ici également les mesures « spectateur » et « post-sélection », qui sont nécessaires pour effectuer la post-sélection sur les échantillons générés par le Executor.

# Build template circuit and samplex for later use with the "Executor"
template_circuit, samplex = samplomatic.build(boxed_circuit)

# Add post-selection instructions to the template circuit
post_selection_pm = PassManager(
    [
        AddSpectatorMeasures(backend.coupling_map),
        AddPostSelectionMeasures(x_pulse_type="rx"),
    ]
)
template_circuit = post_selection_pm.run(template_circuit)
draw_circ = template_circuit.copy_empty_like()
draw_circ.data = template_circuit.data[:324]
draw_circ.draw("mpl", fold=-1, scale=0.3, idle_wires=False)

Output:

Output of the previous code cell

Apprenez à reconnaître ce bruit à l'aide de NoiseLearnerV3

Avant de pouvoir utiliser le PNA pour atténuer les erreurs, nous devons d’abord caractériser le bruit agissant sur chaque couche unique à deux qubits et sur la couche de mesure de notre circuit. Pour ce faire, nous utilisons le NoiseLearnerV3 programme afin d'apprendre de manière expérimentale des modèles de bruit pour chaque couche identifiée précédemment. L'apprenant réalise des expériences de type « benchmarking » qui permettent d'estimer le bruit du canal affectant chaque couche, puis renvoie un objet de résultat contenant le modèle appris.

Nous commençons par identifier les couches uniques de notre circuit à l'aide de find_unique_box_instructions provenant de samplomatic. Cela nous permet de n'apprendre le bruit qu'une seule fois par type de couche distinct, ce qui réduit au minimum le nombre d'expériences et le coût total des essais. La liste des couches ainsi obtenue est transmise à l'algorithme d'apprentissage du bruit.

Il existe quelques paramètres clés qui déterminent la manière dont le bruit est appris :

  • num_randomizations: Nombre de circuits aléatoires utilisés par configuration d'apprentissage.
  • shots_per_randomization: Nombre de tirs effectués par circuit d'apprentissage aléatoire.
  • layer_pair_depths: Les profondeurs du circuit (mesurées en nombre de paires) à utiliser dans les expériences d'apprentissage.
  • post_selection: Permet une post-sélection basée sur les fronts à l'aide de rx portes pour appliquer des impulsions post-mesure.
# Noise learning parameters
num_randomizations_nl = 64
shots_per_randomization_nl = 128

# Match the deepest noise-learning depth to the depth of the circuit being
# mitigated. ``layer_pair_depths`` are measured in layer pairs (a layer + its
# inverse), so the deepest value is half the circuits's two-qubit-layer depth.
# Learning to this depth improves the quality of the mitigation.

depth_2q = isa_circuit.depth(lambda instr: instr.operation.num_qubits == 2)
max_layer_pair_depth = depth_2q // 2

# Use a fixed schedule of learning depths, but drop any that exceed the circuit's
# depth and always cap the deepest value at ``max_layer_pair_depth`` so we never
# learn deeper than the circuit being mitigated
candidate_depths = [1, 2, 4, 8, 12, 16, 24, 32, 40, 48]
layer_pair_depths = sorted(
    {d for d in candidate_depths if d < max_layer_pair_depth}
    | {max_layer_pair_depth}
)

# Find the unique instructions (layers) from the boxed-up circuit
unique_2q_layers_and_meas = find_unique_box_instructions(
    boxed_circuit, normalize_annotations=None, undress_boxes=True
)

# Configure and run the noise learner on the unique layers.
# Options can be passed directly as a dictionary.
noise_learner_options = {
    "num_randomizations": num_randomizations_nl,
    "shots_per_randomization": shots_per_randomization_nl,
    "layer_pair_depths": layer_pair_depths,
    "post_selection": {
        "enable": True,
        "strategy": "edge",
        "x_pulse_type": "rx",
    },
    "environment": {"job_tags": ["TUT_PNA"]},
}

noise_learner = NoiseLearnerV3(backend, noise_learner_options)
noise_learner_job = noise_learner.run(unique_2q_layers_and_meas)
noise_learner_result = noise_learner_job.result()

Visualiser les taux de bruit appris

Une fois les modèles de bruit appris, nous pouvons examiner la distribution des taux d'erreur inférés pour les opérations à un qubit et à deux qubits. Le code ci-dessous extrait les représentations de Pauli-Lindblad à partir des résultats d'apprentissage du bruit et recueille les taux de bruit correspondants.

Pour chaque couche apprise :

  • Nous convertissons le modèle de bruit en une liste clairsemée de (pstr, qubits, rate) tuples, où pstr représente la chaîne de Pauli agissant sur les qubits donnés et rate le taux d'erreur associé.
  • Nous décomposons les taux en termes à un qubit (len(pstr) == 1) et à deux qubits (len(pstr) == 2).
  • Les listes de taux sont ensuite triées, puis leurs valeurs médianes sont calculées.

Nous représentons graphiquement les distributions des taux de bruit pour un qubit (en rouge) et deux qubits (en bleu) sur une échelle logarithmique, leurs valeurs médianes étant indiquées par des lignes verticales, afin de pouvoir comparer les amplitudes relatives des générateurs de Pauli-Lindblad appris. L'ordre des taux pour les qubits simples et doubles dépend du dispositif et des couches spécifiques caractérisées; dans cette série d'essais, ce sont les générateurs à qubit unique ( weight-1 ) qui présentent le taux médian le plus élevé.

hw_rates_1q = []
hw_rates_2q = []
for nlr in noise_learner_result[:2]:
    plm_list = nlr.to_pauli_lindblad_map().to_sparse_list()
    hw_rates_1q += [
        rate for (pstr, qubits, rate) in plm_list if len(pstr) == 1
    ]
    hw_rates_2q += [
        rate for (pstr, qubits, rate) in plm_list if len(pstr) == 2
    ]
hw_rates_1q = sorted(hw_rates_1q)
hw_rates_2q = sorted(hw_rates_2q)
median_1q = hw_rates_1q[len(hw_rates_1q) // 2]
median_2q = hw_rates_2q[len(hw_rates_2q) // 2]
fig, ax = plt.subplots(1, 1, figsize=(14, 5))
ax.scatter(
    (hw_rates_1q),
    [(i) / (len(hw_rates_1q) - 1) for i in range(len(hw_rates_1q))],
    color="red",
    label="1q rates",
)
ax.set_xscale("log")
ax.set_ylim(0, 1.1)
ax.vlines(median_1q, 0, 1, color="red")
ax.text(median_1q * 1.1, 0.1, f"{median_1q:.2e}")
ax.scatter(
    (hw_rates_2q),
    [(i) / (len(hw_rates_2q) - 1) for i in range(len(hw_rates_2q))],
    color="blue",
    label="2q rates",
)
ax.set_xscale("log")
ax.set_ylim(0, 1.1)
ax.vlines(median_2q, 0, 1, color="blue")
ax.text(median_2q * 1.1, 0.2, f"{median_2q:.2e}")
ax.set_title("Learned noise rates")
ax.set_xlabel("Noise rate")
ax.set_yticks([])
plt.legend()

Output:

<matplotlib.legend.Legend at 0x125336120>
Output of the previous code cell

Associer des boîtiers de circuit à un bruit appris

Une fois que nous avons obtenu les modèles de bruit pour chaque couche unique à deux qubits, nous devons les associer aux annotations correspondantes InjectNoise au sein du circuit encadré.

La InjectNoise directive est une samplomatic annotation qui utilise les « dressers » à qubit unique pour injecter du bruit dans le circuit de manière contrôlée et configurable. Cela permet une modélisation modulaire du bruit à travers différentes couches.

Chaque InjectNoise annotation comprend :

  • InjectNoise.ref - un identifiant unique pour l'annotation. Cette information est utilisée par l'objet samplex pour attribuer correctement le modèle de bruit correspondant.
  • InjectNoise.modifier_ref (facultatif) - une référence secondaire permettant de mettre à l'échelle le modèle de bruit attribué à l'aide d'un facteur multiplicatif.

Au cours de cette étape, nous établissons une correspondance entre chaque InjectNoise.ref et le modèle de bruit appris qui lui correspond (PauliLindbladMap). Cette correspondance garantit que chaque couche de portes d'intrication du circuit est associée au modèle de bruit approprié, de sorte que les effets du bruit soient appliqués avec précision lors de l'échantillonnage et des étapes suivantes d'atténuation du bruit.

# map inject noise refs to pauli lindblad maps
refs_to_noise_models = {}
for instruction, result in zip(
    unique_2q_layers_and_meas, noise_learner_result, strict=False
):
    if inject_noise_annot := get_annotation(
        instruction.operation, InjectNoise
    ):
        refs_to_noise_models[inject_noise_annot.ref] = (
            result.to_pauli_lindblad_map()
        )

Propager l'observable à travers l'anti-bruit appris

Comme indiqué plus haut, cette opération s'effectue en deux étapes. Tout d'abord, nous étendons un générateur anti-bruit jusqu'à l'extrémité du circuit. Ensuite, nous appliquons l'observable à ce générateur ayant subi une évolution. Ce processus est répété pour chaque générateur anti-bruit du circuit. Dans cette implémentation, chaque générateur d'une couche donnée est propagé en parallèle jusqu'à l'extrémité du circuit. De plus, le multitraitement Python est utilisé pour effectuer en parallèle à la fois la propagation avant de l'anti-bruit et la propagation arrière de l'observable. Cela permet d'éviter l'accumulation de générateurs évolués en mémoire et d'optimiser les ressources de calcul.

Lorsque vous exécutez PNA, vous devez toujours fournir un circuit sujet au bruit et une grandeur observable. Si votre circuit « bruyant » est un circuit encadré comportant InjectNoise des annotations, vous devez fournir le mappage que nous avons créé à l'étape précédente. On peut également passer un circuit non encapsulé contenant PauliLindbladError des instructions provenant de qiskit-aer. Dans ce cas, refs_to_noise_models il n'est pas nécessaire de le fournir. Outre les données principales, tenez également compte des éléments suivants :

  • max_err_terms: Nombre de termes à conserver dans chaque générateur anti-bruit au fur et à mesure de la propagation vers l'avant. Augmenter cette valeur améliore généralement la précision, mais il n'est pas garanti que ce comportement soit monotone.
  • max_obs_terms: Le nombre de termes à conserver dans l'observable d'atténuation du bruit, O~\tilde{O}, lors de sa rétropropagation à travers l'anti-bruit évolué. Des valeurs plus élevées améliorent généralement la précision, mais cette amélioration n'est pas nécessairement monotone.
  • num_processes: Le nombre de cœurs à affecter au processus. N'oubliez pas que les générateurs sont propagés vers l'avant et appliqués à l'observable en parallèle.
  • search_step: L'étape de rétropropagation utilise une méthode gloutonne pour conjuguer approximativement deux opérateurs dans la base de Pauli. Il est possible d'accélérer cette méthode en augmentant search_step. Voir le pauli-prop Consultez la documentation pour plus d'informations.
  • num_to_measure: Bien que cette variable ne soit pas une donnée d'entrée pour generate_noise_mitigating_observable, nous l'utilisons pour déterminer le nombre de termes de O~\tilde{O} que nous souhaitons réellement mesurer. Ici, nous ne prenons en compte que les 30 premiers termes, qui correspondent aux termes d'origine de notre observable. Les termes ont désormais été rééchelonnés de telle sorte que leur mesure permette d'atténuer le bruit de porte acquis. Même si nous ne mesurons que 30 termes à partir de O~\tilde{O}, il est souvent utile de laisser cette valeur atteindre une valeur élevée, car cela augmente la précision des facteurs d'échelle des termes dominants.
# PNA parameters
num_processes = 8
max_err_terms = 10_000
max_obs_terms = 10_000
num_to_measure = num_qubits

obs_tilde_isa = generate_noise_mitigating_observable(
    boxed_circuit,
    isa_observable,
    refs_to_noise_models,
    max_err_terms=max_err_terms,
    max_obs_terms=max_obs_terms,
    num_processes=num_processes,
    print_progress=True,
    search_step=8,
)
p_2_v = {p: v for v, p in enumerate(layout)}
obs_tilde_virtual = SparsePauliOp.from_sparse_list(
    [
        (pstr, [p_2_v[p] for p in p_qubits], coeff)
        for (pstr, p_qubits, coeff) in obs_tilde_isa.to_sparse_list()
    ],
    num_qubits=num_qubits,
)
obs_tilde_virtual = obs_tilde_virtual[
    np.argsort(np.abs(obs_tilde_virtual.coeffs))[::-1]
][:num_to_measure]

Output:

Finished! 13740 / 13740 generators propagated.                       
obs_tilde_isa = obs_tilde_isa[np.argsort(np.abs(obs_tilde_isa.coeffs))][::-1]
plt.xscale("log")
plt.yscale("log")
plt.title(r"$\tilde{O}$ coeff magnitudes")
plt.ylabel("Magnitude")
plt.xlabel("Pauli term index")
plt.plot(np.abs(obs_tilde_isa.coeffs), ".")

Output:

[<matplotlib.lines.Line2D at 0x124b0a120>]
Output of the previous code cell

Mettre les bases de mesure sous forme canonique

Ensuite, on détermine un ensemble minimal de bases de mesure permettant de couvrir intégralement chaque terme de Pauli dans l'observable mesurée ( plusieurs observables peuvent être mesurées simultanément si elles commutent au niveau des qubits ). Étant donné que nous ne mesurons que les termes de notre observable d'origine, qui est la somme de tous les Paulis simplesZ , une seule base suffit : la base « all-Z ».

Outre la recherche d'un ensemble de bases de mesure de Pauli, nous devons mettre ces termes de Pauli en correspondance avec la forme canonique attendue par le Executor. Pour plus d'informations sur l'ordre canonique des qubits, consultez la documentation de Samplomatic.

meas_box = boxed_circuit.data[-1]
canonical_qubits = [
    idx
    for idx, qubit in enumerate(boxed_circuit.qubits)
    if qubit in meas_box.qubits
]
c_2_p = {
    c: p for c, p in enumerate(canonical_qubits)
}  # canonical -> physical
p_2_v = {p: v for v, p in enumerate(layout)}  # physical -> virtual
c_2_v = {c: p_2_v[p] for c, p in c_2_p.items()}  # canonical -> virtual
meas_bases, bases_reverser = get_measurement_bases(obs_tilde_virtual)
meas_bases_canonical = [
    np.array([base[c_2_v[c]] for c in range(num_qubits)], dtype=np.uint8)
    for base in meas_bases
]

Étape 3 : Réaliser des expériences quantiques

Précisez la méthode d'échantillonnage dans le QuantumProgram

Nous allons maintenant configurer le QuantumProgram, qui sert de conteneur central pour tous les circuits et toutes les configurations d'échantillonnage qui seront exécutés par le Executor. Cet objet définit la manière dont les instances de circuits aléatoires sont générées, regroupées par lots et exécutées afin de produire les résultats de mesure utilisés dans PNA.

Un QuantumProgram peut contenir plusieurs éléments, chacun composé d'un circuit modèle et d'un objet correspondant samplex qui définit la manière dont les randomisations sont appliquées. Cette abstraction permet au Executor de gérer l'ensemble du flux de travail sous la forme d'un programme unique et modulaire — depuis la génération aléatoire de circuits jusqu'à la collecte et l'agrégation des prises.

Au cours de cette étape, nous créons un programme QuantumProgram qui exécute notre expérience PNA à l'aide du circuit type et samplex de l'élément que nous avons construits précédemment. La configuration comprend les éléments suivants :

  • template_circuit: Le circuit contenant toutes les portes logiques nécessaires pour mettre en œuvre toutes les randomisations souhaitées (qu'il s'agisse de randomisations de type « twirling », de paramètres, etc.).
  • samplex: Objet définissant une distribution de probabilité sur l'ensemble des randomisations possibles du circuit, à partir desquelles un échantillon sera prélevé.
  • samplex_arguments: Liaisons nécessaires pour définir complètement le samplex
    • basis_changes: C'est ici que nous spécifions un ensemble de bases de mesure qui couvrira tous les termes de Pauli dans l'observable mesurée.
    • noise_scales.ref: Nous avons réglé le niveau de chaque couche de bruit sur 0.0 afin d'éviter que du bruit supplémentaire ne soit introduit dans nos échantillons.
    • pauli_lindblad_maps: Obligatoire si noise_scales sont transmis. Cela permet simplement d'associer les couches de bruit au modèle de bruit correspondant.
  • shape: Un tuple de forme permettant d'étendre la forme implicite définie par samplex_arguments. Les axes non triviaux introduits par cette extension énumèrent les randomisations.
# Control the # of shots during execution
shots_per_randomization_exec = 64
num_randomizations_exec = 6144

# Zero out the noise to prevent noise from being injected during execution.
# We only added InjectNoise annotations so PNA could associate the noise
# to layers in the circuit
samplex_inputs = {f"noise_scales.{ref}": 0.0 for ref in refs_to_noise_models}
samplex_inputs |= {"pauli_lindblad_maps": refs_to_noise_models}

# Specify the bases to measure. The samplex exposes one basis-change input per
# ChangeBasis-annotated box; here a single all-Z basis covers every term. We
# look up the basis-change interface name rather than hardcoding an index, since
# the name depends on the circuit's box structure.
bases_broadcastable = np.expand_dims(np.array(meas_bases_canonical), axis=1)
samplex_inputs |= {
    spec.name: bases_broadcastable
    for spec in samplex.inputs().get_specs(r"^basis_changes\.")
}

# Convert samplex_inputs into a dict to pass to QuantumProgram
samplex_arguments = (
    samplex.inputs().make_broadcastable().bind(**samplex_inputs)
)

# Instantiate the QuantumProgram with the specified parameters
program = QuantumProgram(shots=shots_per_randomization_exec)
program.append_samplex_item(
    circuit=template_circuit,
    samplex=samplex,
    samplex_arguments=samplex_arguments,
    shape=(num_randomizations_exec,),
)

Échantillonnez le circuit à l'aide du Executor

Maintenant que nous avons défini notre QuantumProgram, la réalisation de l'expérience est très simple. Il suffit de créer une instance de Executor l'objet, de lui fournir le backend, puis de lancer le programme.

# Execute (sample) the circuit
executor = Executor(backend)
job_exec = executor.run(program)
exec_results = job_exec.result()

Étape 4 : Reconstitution et analyse des résultats

Pour calculer une valeur attendue corrigée de l'erreur, on procède comme suit :

  • Calculer les facteurs d'échelle TREX en fonction du bruit identifié affectant les mesures,
  • Générer un masque permettant de ne conserver que les échantillons sélectionnés, et
  • Utilisez la executor_expectation_values fonction de qiskit-addon-utils pour regrouper toutes les données en une valeur attendue corrigée des erreurs.
# Computing the TREX factors
measurement_noise_map = noise_learner_result[2].to_pauli_lindblad_map()
trex_rescale_factors = trex_factors(measurement_noise_map, bases_reverser)

# Post-select the results
post_selector = PostSelector.from_circuit(
    circuit=template_circuit, coupling_map=backend.coupling_map
)

# Compute the ps mask for filtering results
mask = post_selector.compute_mask(exec_results[0], strategy="edge")

# Compute expvals using post selected results
results = executor_expectation_values(
    exec_results[0]["meas"],
    bases_reverser,
    meas_basis_axis=0,
    avg_axis=1,
    measurement_flips=exec_results[0]["measurement_flips.meas"],
    pauli_signs=exec_results[0].get("pauli_signs", None),
    postselect_mask=mask,
    rescale_factors=trex_rescale_factors,
)

Comparer les stratégies d'atténuation : PNA, PNA+TREX, PNA+PS, PNA+PS+TREX

À partir de ces Executor résultats, nous calculons et visualisons les valeurs attendues pour plusieurs variantes de mesures d'atténuation.

bases_reverser_unmit = {Pauli("Z" * num_qubits): [observable]}
args = [
    (bases_reverser_unmit, None, None),
    (bases_reverser, None, None),
    (bases_reverser, None, trex_rescale_factors),
    (bases_reverser, mask, None),
    (bases_reverser, mask, trex_rescale_factors),
]

evs = []
for reverser, postsel_mask, factors in args:
    # Compute expvals using post selected results
    res_ps = executor_expectation_values(
        exec_results[0]["meas"],
        reverser,
        meas_basis_axis=0,
        avg_axis=1,
        measurement_flips=exec_results[0]["measurement_flips.meas"],
        pauli_signs=exec_results[0].get("pauli_signs", None),
        postselect_mask=postsel_mask,
        rescale_factors=factors,
    )
    res_ps = np.array(res_ps)
    evs.append(res_ps[:, 0][0])

experiments = ["PNA", "PNA+TREX", "PNA+PS", "PNA+PS+TREX"]
colors = ["#d9d9d9", "#b0b0b0", "#7f7f7f", "#4c4c4c"]
plt.bar(experiments, evs[1:], color=colors)
plt.axhline(y=1, color="green", linestyle="--", linewidth=2, label="Ideal")
plt.axhline(
    y=evs[0], color="red", linestyle="--", linewidth=2, label="Unmitigated"
)
plt.ylabel("Expectation value", fontsize=14)

plt.title(
    r"30q Mirrored Ising, 10 Trotter steps, $\theta_{rx}=\frac{\pi}{8}$",
    fontsize=14,
)
plt.legend(loc="upper left", bbox_to_anchor=(1.05, 1), borderaxespad=0.0)
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

Output:

Output of the previous code cell

Les résultats démontrent les avantages cumulés de la combinaison de différentes techniques d'atténuation des erreurs. L'approche PNA simple permet déjà de ramener la valeur attendue à un niveau proche de la référence idéale, ce qui indique que la propagation de canaux de bruit inverses dans l'observable compense efficacement les erreurs des portes à deux qubits.

  • L'ajout de la pondération TREX (PNA+TREX) améliore légèrement l'estimation en corrigeant le déséquilibre d'échantillonnage dans les circuits randomisés.
  • La post-sélection (PNA+PS) apporte un gain plus notable en éliminant les résultats de mesure incohérents, qui sont probablement dus à des erreurs résiduelles.
  • Enfin, la combinaison des trois (PNA+PS+TREX) donne le résultat le plus précis, qui se rapproche fortement de la valeur idéale, ce qui montre à quel point ces stratégies d'atténuation se renforcent mutuellement.

Dans l'ensemble, cette comparaison met en évidence que la méthode PNA constitue une base solide pour l'estimation de la valeur attendue tenant compte du bruit, tandis que TREX et la post-sélection apportent des améliorations complémentaires permettant d'augmenter encore la précision.


Etapes suivantes

Si ce travail vous a paru intéressant, les documents suivants pourraient vous intéresser :

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