Aller au contenu principal
Ce projet est en phase bêta. Tu as trouvé un bug ? Ouvrir un ticket GitHub. Une question ou une idée ? Pose ta question dans Discussions — tes retours sont les bienvenus !
Quelle est la qualité de la traduction sur cette page ?

Améliorer les valeurs d'espérance avec l'absorption de bruit propagée (PNA)

Estimation d'utilisation : 10 minutes sur un processeur Heron (REMARQUE : Il s'agit uniquement d'une estimation. Votre temps d'exécution peut varier.)

Résultats d'apprentissage​

  • Ce qu'est l'absorption de bruit propagée (PNA), et comment elle atténue le bruit des portes à deux qubits en absorbant des canaux de bruit inverse appris dans l'observable mesurée

  • Comment utiliser samplomatic pour encadrer et annoter les couches de circuit pour le twirling, les changements de base et l'injection de bruit

  • Comment apprendre le bruit de couche avec [NoiseLearnerV3](/guides/directed-execution-model#noiselearnerv3) et le propager dans une observable atténuant le bruit avec qiskit-addon-pna

  • Comment échantillonner des circuits randomisés avec les classes QuantumProgram et Executor dans qiskit-ibm-runtime, et combiner PNA avec TREX et la post-sélection

Prérequis​

Contexte​

Dans ce tutoriel, nous démontrons comment exploiter des outils avancés d'atténuation d'erreur dans Qiskit pour améliorer l'estimation des valeurs d'espérance dans des expériences quantiques bruitées.

Qu'est-ce que l'absorption de bruit propagée (PNA) ?​

L'absorption de bruit propagée est une technique d'atténuation des erreurs de porte en propageant l'observable à travers le canal de bruit inverse affectant les portes à deux qubits, ce qui donne une observable atténuant le bruit.

Nous pouvons utiliser l'atténuation d'erreur quantique pour extraire des valeurs d'espérance utiles à partir de matériel quantique bruité sans nécessiter de tolérance aux pannes complète. Le PNA se concentre spécifiquement sur l'absorption des effets de bruit dans l'observable elle-même plutôt que sur la modification des opérations du circuit.

Chaque porte bruitée 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'insérer les opérations inverses dans l'exécution matérielle (ce qui est typiquement infaisable), le PNA propage les canaux de bruit inverses vers l'avant à travers le circuit et les applique à l'observable. Ce processus transforme l'observable OO en un nouvel opérateur O~\tilde{O}, tel que mesurer O~\tilde{O} sur le circuit bruité donne la même valeur d'espérance que mesurer OO sur un circuit idéal, sans bruit.

Cela peut être résumé comme suit :

  1. Modéliser chaque porte bruitée uiu_i comme une opération idéale suivie d'un canal de bruit Λi\Lambda_i.
  2. Apprendre ou estimer chaque Λi\Lambda_i à l'aide d'outils de caractérisation du bruit.
  3. Définir et propager les cartes de bruit inverses Λi−1\Lambda_i^{-1} vers l'avant à travers le circuit en utilisant des techniques de transfert de Pauli.
  4. Absorber ces inverses dans l'observable, ce qui donne un opérateur atténué en bruit 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 épars), cette propagation peut être effectuée efficacement en utilisant la propagation de Pauli. La propagation de Pauli fournit un cadre pour approximer comment les canaux de bruit inverses se transforment lorsqu'ils traversent des couches d'opérations de Clifford et non-Clifford, tout en contrôlant la complexité computationnelle.

En déplaçant l'atténuation vers l'observable, le PNA évite le grand surcoût d'échantillonnage qui découlerait autrement de l'insertion d'opérations de correction physiques dans le circuit. Au lieu de cela, le circuit bruité original est exécuté, tandis que l'observable est transformée en un nouvel opérateur O~\tilde{O} dont la valeur d'espérance annule les effets du bruit.

Le workflow PNA​

Le processus peut être compris à travers les étapes conceptuelles suivantes. Le premier schéma illustre une expérience bruitée standard.

Noisy experiment

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

Noise-mitigated experiment

Au lieu d'implémenter le canal de bruit inverse en l'échantillonnant sur le QPU comme dans l'annulation d'erreur probabiliste (PEC), nous l'appliquons classiquement à l'observable mesurée en utilisant la propagation de Pauli. L'observable résultante atténue efficacement le bruit de porte appris lorsqu'elle est mesurée.

PNA overview

Atténuation d'erreur modulaire avec Samplomatic et Executor​

L'approche PNA s'appuie sur une architecture d'atténuation d'erreur modulaire dans Qiskit. Cette architecture utilise la bibliothèque samplomatic avec les classes QuantumProgram et Executor (ajoutées à Qiskit Runtime dans qiskit-ibm-runtime v0.47.0) pour rendre des techniques telles que l'absorption de bruit propagée et le twirling de Pauli composables et réutilisables à travers différentes expériences.

Plutôt que d'intégrer la logique d'atténuation directement dans la définition du circuit, l'atténuation est exprimée de manière déclarative à travers des annotations samplomatic et gérée de manière programmatique via Executor, qui contrôle comment les circuits randomisés sont générés, exécutés et post-traités.

Dans ce tutoriel, nous mettons en œuvre un modèle Qiskit pour démontrer comment le PNA peut propager des canaux de bruit de Pauli inverses et modifier l'observable en conséquence, afin d'améliorer l'estimation des valeurs d'espérance sur des QPU bruités.

Vue d'ensemble du flux de travail​

  • Étape 1 : Mapper le problème quantique

    • Construire un modèle d'Ising pulsé (kicked Ising), trotterisé et miroir, ainsi qu'une observable cible.
  • Étape 2 : Caractériser et propager le bruit

    • Utiliser samplomatic pour identifier et annoter les couches à deux qubits uniques et les mesures dans le circuit.

    • Apprendre le bruit affectant chaque couche unique à l'aide de NoiseLearnerV3.

    • Associer chaque annotation InjectNoise à son modèle de bruit appris correspondant.

    • Utiliser qiskit-addon-pna pour propager les canaux de bruit inverse vers l'avant à travers le circuit et les absorber dans l'observable cible.

  • Étape 3 : Exécuter les expériences quantiques

    • Définir un QuantumProgram pour spécifier un échantillonnage aléatoire via samplex et exécuter les expériences sur le backend à l'aide de l'Executor.
  • Étape 4 : Reconstruire et analyser les 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 non atténués.

Prérequis​

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

  • Qiskit SDK v2.2 ou ultérieur, avec la prise en charge de la visualisation

  • Qiskit Runtime v0.47 ou ultérieur (pip install qiskit-ibm-runtime)

  • Samplomatic v0.13 ou ultérieur (pip install samplomatic)

  • L'addon Qiskit PNA (pip install qiskit-addon-pna)

  • Les utilitaires d'addon Qiskit (pip install qiskit-addon-utils)

Configuration​

# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util

_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_pna": "qiskit-addon-pna", "qiskit_addon_utils": "qiskit-addon-utils", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "samplomatic": "samplomatic"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
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​

PNA atténue le bruit physique des portes à deux qubits d'un processeur quantique spécifique. Le flux de travail dépend de deux services matériels qui n'ont pas d'analogue significatif sur un simulateur idéal :

  • NoiseLearnerV3 caractérise expérimentalement le canal de bruit de Pauli-Lindblad épars associé à chaque couche unique de deux qubits du circuit transpilé. Sur un simulateur sans bruit, il n'y a pas de bruit à apprendre, et le canal inverse propagé serait l'identité.

  • La primitive Executor échantillonne les circuits aléatoirisés et « twirlés » générés par samplomatic sur un backend.

En principe, tu pourrais substituer un modèle de bruit synthétique. Par exemple, tu pourrais associer des instructions PauliLindbladError avec Qiskit Aer et transmettre le circuit bruité résultant directement à generate_noise_mitigating_observable. Cependant, cela ne fait que valider la comptabilité classique par rapport à un bruit que tu as injecté toi-même, et masque l'intérêt de la technique. C'est pourquoi nous passons l'exemple du simulateur et démontrons le flux de travail PNA complet directement sur le matériel, avec chaque étape du modèle Qiskit détaillée ci-dessous.

Exemple matériel à grande échelle​

Nous allons maintenant exécuter le flux de travail PNA complet sur un modèle d'Ising pulsé (kicked Ising) à 30 sites exécuté sur du matériel IBM Quantum®, en suivant les quatre étapes d'un modèle Qiskit.

Étape 1 : Mapper vers un problème quantique​

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

Pour cette expérience, nous allons étudier la dynamique temporelle d'un modèle d'Ising pulsé (kicked Ising) à 30 sites sur une chaîne de spins 1D. L'hamiltonien considéré est :

H=−J∑⟨i,j⟩ZiZj+h∑iXiH = -J\sum\limits_{\langle i,j \rangle} Z_iZ_j + h\sum\limits_iX_i,

où J>0J>0 décrit le couplage des spins voisins les plus proches, i<ji<j, et le champ transverse global, hh, est fixé à π8\frac{\pi}{8}. Plus hh est éloigné d'un angle de Clifford (c'est-à-dire θ=nπ2,n∈Z\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 l'observable, nous considérerons l'aimantation moyenne par site, 1N∑i=1N⟨zi⟩\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 of the previous code cell

Étape 2 : Optimiser le problème pour l'exécution matérielle​

L'étape suivante consiste à préparer notre circuit de Trotter miroir pour son exécution sur du véritable matériel IBM Quantum. Exécuter sur un QPU nécessite plus que la simple construction du circuit abstrait, car nous devons l'optimiser afin que :

  1. Il respecte l'ensemble de portes natives et la connectivité du backend. La transpilation mappe le circuit logique en un circuit ISA pris en charge par le backend cible. Cela garantit que chaque porte et interaction de qubits est physiquement réalisable.

  2. Nous puissions caractériser le bruit au niveau des couches de circuit. PNA repose sur l'apprentissage et la propagation de canaux de bruit inverse. Pour ce faire efficacement, nous divisons le circuit transpilé en couches uniques de deux qubits « encadrées » (boxed). Ces boîtes nous permettent d'associer chaque couche du circuit à son propre modèle de bruit appris.

  3. Nous puissions alimenter PNA avec des modèles de bruit réalistes. Une fois le circuit encadré, nous utilisons le service NoiseLearnerV3 pour apprendre expérimentalement les canaux de bruit de Pauli affectant chaque couche unique de deux qubits. Ces modèles appris sont ensuite reliés au circuit via les annotations de Samplomatic.

Dans l'ensemble, cette étape comble l'écart entre un circuit miroir idéalisé et un circuit prêt pour le matériel avec des modèles de bruit appris. Avec cette configuration, PNA peut propager des canaux de bruit inverse à travers le circuit et ajuster l'observable en conséquence.

Se connecter au backend et transpiler en un circuit ISA​

Nous initialisons d'abord le IBM Quantum Compute Service et sélectionnons un backend. Authentifie-toi avec ton propre compte en suivant les instructions pour enregistrer tes identifiants, après quoi QiskitRuntimeService() les récupérera 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}")
Selected backend: ibm_fez

Ensuite, nous choisissons une chaîne connectée de qubits sur le backend et transpilons le circuit dessus.

En utilisant l'assistant find_qubit_chain défini dans la section Configuration, nous sélectionnons une ligne de num_qubits qubits physiques connectés. Nous transpilons ensuite avec optimization_level=0 et fixons cette chaîne comme initial_layout, ce qui préserve exactement la structure des couches de portes à deux qubits du circuit miroir. Cette structure est ce dont dépendent les étapes d'encadrement et d'apprentissage du bruit, donc un niveau d'optimisation plus élevé (qui annulerait les portes miroir) doit être évité ici.

# 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 of the previous code cell

Twirler les couches de portes à deux qubits et les mesures, et trouver les couches uniques​

Nous utilisons samplomatic pour encadrer le circuit et identifier les couches uniques de deux qubits. Une boîte est une construction qui regroupe des instructions afin que des intentions ou annotations spécifiques puissent ensuite être appliquées uniformément à toutes les portes au sein de la même boîte.

Ici, nous appelons la méthode generate_boxing_pass_manager, qui va au-delà de la simple identification des couches à deux qubits. Elle effectue plusieurs tâches clés :

  • Regroupe toutes les couches à deux qubits du circuit,

  • Applique les annotations Twirl et ChangeBasis à ces couches,

  • Regroupe les opérations de mesure dans leurs propres sections encadrées, et

  • Applique l'annotation InjectNoise à chaque couche à deux qubits.

Ces annotations définissent la manière dont le bruit, les changements de base et le twirling sont gérés tout au long du circuit. Elles établissent également la structure utilisée plus tard pour apprendre et atténuer le bruit.

Les principales options de configuration sont :

  • enable_gates/enable_measures: True : Encadrer toutes les couches de portes à deux qubits et les mesures terminales. Les portes à un qubit sont habillées à gauche (left-dressed) dans les boîtes.

  • measure_annotations: all : Inclure les annotations Twirl et ChangeBasis sur la boîte de mesure.

  • twirling_strategy: active : Twirler tous les qubits actifs dans chaque boîte contenant des portes intriquantes.

  • inject_noise_targets: gates : Ajouter des annotations InjectNoise à toutes les boîtes annotées Twirl contenant des portes intriquantes.

  • inject_noise_strategy: uniform_modification : Mettre à l'échelle toutes les couches de bruit de manière équivalente à travers le 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 of the previous code cell

Générer le circuit modèle et le samplex, qui définissent comment le circuit sera échantillonné.

Ici, nous ajoutons également des mesures spectatrices et de post-sélection, qui sont nécessaires pour effectuer une post-sélection sur les échantillons produits par l'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 of the previous code cell

Apprendre le bruit avec NoiseLearnerV3​

Avant de pouvoir appliquer PNA pour l'atténuation d'erreur, nous devons d'abord caractériser le bruit agissant sur chaque couche unique de deux qubits et sur la couche de mesure de notre circuit. Pour ce faire, nous utilisons le programme NoiseLearnerV3 afin d'apprendre expérimentalement des modèles de bruit pour chaque couche identifiée précédemment. L'apprenant effectue des expériences de type benchmarking qui estiment le canal de bruit affectant chaque couche et renvoie un objet résultat contenant le modèle appris.

Nous commençons par identifier les couches uniques dans notre circuit à l'aide de find_unique_box_instructions de samplomatic. Cela garantit que nous n'apprenons le bruit qu'une seule fois par type de couche distinct, minimisant ainsi le nombre d'expériences et le coût total en shots. La liste résultante de couches est transmise à l'apprenant de bruit.

Il existe quelques paramètres clés qui contrôlent 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 shots pris par circuit d'apprentissage aléatoire.

  • layer_pair_depths : Les profondeurs de circuit (mesurées en nombre de paires) à utiliser dans les expériences d'apprentissage.

  • post_selection : Active la post-sélection basée sur les arêtes en utilisant des portes rx 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​

Après avoir appris les modèles de bruit, 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 des résultats de bruit appris et rassemble les taux de bruit correspondants.

Pour chaque couche apprise :

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

Nous traçons les distributions des taux de bruit à un qubit (rouge) et à deux qubits (bleu) sur une échelle logarithmique, avec leurs valeurs médianes marquées par des lignes verticales, afin de pouvoir comparer les magnitudes relatives des générateurs de Pauli-Lindblad appris. L'ordre des taux à un et deux qubits dépend de l'appareil et des couches spécifiques caractérisées ; dans cette exécution, les générateurs à un qubit (poids 1) portent 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()
<matplotlib.legend.Legend at 0x125336120>

Output of the previous code cell

Associer les boîtes de circuit au bruit appris​

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

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

Chaque annotation InjectNoise inclut :

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

Dans cette étape, nous construisons un mappage de chaque InjectNoise.ref vers son modèle de bruit appris correspondant (PauliLindbladMap). Ce mappage garantit que chaque couche de portes intriquantes du circuit est associée au modèle de bruit approprié, afin que les effets du bruit soient appliqués avec précision lors de l'échantillonnage et des étapes ultérieures 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 évoqué ci-dessus, cela se fait en deux étapes. Premièrement, nous propageons un générateur d'anti-bruit jusqu'à la fin du circuit. Ensuite, nous propageons l'observable à travers ce générateur évolué. Ce processus est répété pour chaque générateur d'anti-bruit du circuit. Dans cette implémentation, chaque générateur d'une couche donnée est propagé jusqu'à la fin du circuit en parallèle. De plus, le multiprocessing Python est utilisé pour effectuer à la fois la propagation vers l'avant de l'anti-bruit et la rétropropagation de l'observable en parallèle. Cela évite une accumulation de générateurs évolués en mémoire et maximise également les ressources de calcul.

Lors de l'exécution de PNA, tu dois toujours fournir un circuit bruité et un observable. Si ton circuit bruité est un circuit encadré avec des annotations InjectNoise, tu dois fournir le mappage que nous avons créé à l'étape ci-dessus. On peut également transmettre un circuit non encadré contenant des instructions PauliLindbladError provenant de qiskit-aer. Dans ce cas, refs_to_noise_models n'a pas besoin d'être fourni. En plus des entrées principales, considère également les éléments suivants :

  • max_err_terms : Le nombre de termes à conserver dans chaque générateur d'anti-bruit au fur et à mesure de sa propagation vers l'avant. Autoriser une valeur plus grande augmente généralement la précision, mais rien ne garantit 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 augmentent généralement la précision, mais ce n'est pas garanti de façon monotone.

  • num_processes : Le nombre de cœurs à dédier au processus. Rappelle-toi 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. Cette méthode peut être accélérée en augmentant search_step. Voir la documentation de pauli-prop pour plus d'informations.

  • num_to_measure : Bien que cette variable ne soit pas une entrée de generate_noise_mitigating_observable, nous l'utilisons pour contrôler combien de termes de O~\tilde{O} nous souhaitons réellement mesurer. Ici, nous mesurons uniquement les 30 termes principaux, qui sont les termes originaux de notre observable. Les termes ont désormais été remis à l'échelle de telle sorte que leur mesure a pour effet d'atténuer le bruit de porte appris. Bien que nous ne mesurions que 30 termes de O~\tilde{O}, il est souvent encore utile de le laisser grandir, car cela augmente la précision des facteurs d'échelle des termes principaux.

# 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]
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), ".")
[<matplotlib.lines.Line2D at 0x124b0a120>]

Output of the previous code cell

Transformer les bases de mesure en forme canonique​

Ensuite, nous trouvons un ensemble minimal de bases à mesurer de manière à couvrir entièrement chaque terme de Pauli dans l'observable mesuré (de nombreux observables peuvent être mesurés simultanément s'ils commutent qubit par qubit). Puisque nous ne mesurons que les termes de notre observable original, qui est la somme de tous les Pauli à un seul Z, une seule base est nécessaire : la base tout-Z.

En plus de trouver un ensemble de bases de mesure de Pauli, nous devons mapper ces termes de Pauli à la forme canonique attendue par l'Executor. Pour plus d'informations sur l'ordre canonique des qubits, consulte la documentation 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 : Exécuter les expériences quantiques​

Spécifier comment échantillonner dans le QuantumProgram​

Nous configurons maintenant le QuantumProgram, qui sert de conteneur central pour tous les circuits et configurations d'échantillonnage qui seront exécutés par l'Executor. Cet objet définit comment les instances de circuit aléatoirisées sont générées, regroupées en lots et exécutées pour produire les résultats de mesure utilisés dans PNA.

Un QuantumProgram peut contenir plusieurs éléments, chacun étant composé d'un circuit modèle et d'un objet samplex correspondant qui définit comment les aléatoirisations sont appliquées. Cette abstraction permet à l'Executor de gérer l'ensemble du flux de travail comme un programme unique et modulaire — de la génération de circuits aléatoirisés à la collecte et l'agrégation des shots.

Dans cette étape, nous créons un QuantumProgram qui exécute notre expérience PNA à l'aide du circuit modèle et du samplex que nous avons construits précédemment. La configuration inclut les éléments suivants :

  • template_circuit : Le circuit contenant toutes les portes nécessaires pour mettre en œuvre toutes les aléatoirisations souhaitées (à partir des aléatoirisations de twirling, des paramètres, etc.).

  • samplex : Un objet définissant une distribution de probabilité sur toutes les aléatoirisations de circuit possibles à partir desquelles échantillonner.

  • samplex_arguments : Les liaisons nécessaires pour définir complètement le samplex

    • basis_changes : C'est ici que nous spécifions un ensemble de bases à mesurer qui couvrira tous les termes de Pauli dans l'observable mesuré.

    • noise_scales.ref : Nous fixons l'échelle de chaque couche de bruit à 0.0 pour empêcher tout bruit supplémentaire d'être injecté dans nos échantillons.

    • pauli_lindblad_maps : Requis si noise_scales est fourni. Cela mappe simplement les couches de bruit au modèle de bruit associé.

  • shape : Un tuple de forme pour étendre la forme implicite définie par samplex_arguments. Les axes non triviaux introduits par cette extension énumèrent les aléatoirisations.

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

Échantillonner le circuit à l'aide de l'Executor​

Maintenant que nous avons défini notre QuantumProgram, l'exécution de l'expérience est simple. Nous instancions simplement l'objet Executor, lui fournissons le backend, et exécutons le programme.

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

Étape 4 : Reconstruire et analyser les résultats​

Pour calculer une valeur d'espérance atténuée en erreur, nous procédons comme suit :

  • Calculer les facteurs d'échelle TREX en fonction du bruit appris affectant les mesures,

  • Générer un masque pour ne conserver que les échantillons post-sélectionnés, et

  • Utiliser la fonction executor_expectation_values de qiskit-addon-utils pour combiner toutes les données en une valeur d'espérance atténuée en erreur.

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

Nous calculons et visualisons les valeurs d'espérance pour plusieurs variantes d'atténuation en utilisant les résultats de l'Executor.

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 of the previous code cell

Les résultats démontrent les avantages cumulatifs de la combinaison de différentes techniques d'atténuation d'erreur. L'approche PNA seule ramène déjà la valeur d'espérance près de la référence idéale, indiquant que la propagation des canaux de bruit inverse dans l'observable compense efficacement les erreurs de portes à deux qubits.

  • L'ajout de la repondération TREX (PNA+TREX) améliore légèrement l'estimation en corrigeant le déséquilibre d'échantillonnage dans les circuits aléatoirisés.

  • La post-sélection (PNA+PS) apporte une amélioration plus notable en filtrant les résultats de mesure incohérents qui résultent probablement d'erreurs résiduelles.

  • Enfin, la combinaison des deux (PNA+PS+TREX) donne le résultat le plus précis, quasiment égal à la valeur idéale, montrant comment ces stratégies d'atténuation se renforcent mutuellement.

Dans l'ensemble, la comparaison souligne que PNA constitue une base solide pour l'estimation de valeurs d'espérance tenant compte du bruit, tandis que TREX et la post-sélection offrent des raffinements complémentaires pour des gains de précision supplémentaires.

Prochaines étapes​

Si tu as trouvé ce travail intéressant, les documents suivants pourraient t'intéresser :