Simuler des systèmes quantiques bruités avec la propagation de Pauli
Dans ce guide, tu utilises le package pauli-prop pour simuler classiquement la dynamique temporelle d'un modèle d'Ising à champ transverse (TFIM) bruité à neuf qubits sur un réseau carré 3x3. Nous utilisons des instructions PauliLindbladError pour définir un canal de bruit, , agissant sur un ensemble de couches d'intrication, . Nous propageons ensuite l'observable, , à rebours à travers le Circuit bruité et estimons les valeurs moyennes pour divers modèles de bruit, ainsi que pour le cas sans bruit.

Lorsque l'observable est propagée à rebours à travers le Circuit, chaque canal de bruit, , associé à la couche d'intrication, , amortit les termes de Pauli de qui anticommutent avec ses générateurs de Pauli-Lindblad. Plus précisément, si est un générateur de Pauli de de taux, , alors un terme de Pauli, , dans se transforme ainsi : , où est le coefficient de . Une fois que a été propagée jusqu'au début du Circuit, la valeur moyenne par rapport à l'état zéro, , peut être calculée trivialement en sommant les coefficients de chaque terme diagonal de (termes contenant ou sur tous les qubits).
Flux de travail :
- Spécifier le réseau du TFIM et utiliser la coloration d'arêtes pour identifier un ensemble minimal de couches d'intrication
- Générer des modèles de bruit synthétiques, , pour chaque couche d'intrication unique,
- Créer des modèles de bruit de différentes échelles pour étudier l'impact du bruit des portes sur le système
- Créer des Circuits quantiques sans bruit et bruités pour les différentes profondeurs et échelles de bruit d'intérêt
- Dans les Circuits bruités, des instructions
PauliLindbladErrorsont insérées avant chaque couche d'intrication
- Dans les Circuits bruités, des instructions
- Utiliser la propagation de Pauli pour simuler les valeurs moyennes exactes du système à différentes profondeurs
- Pour neuf qubits, cela se fait en laissant croître jusqu'à termes, couvrant l'espace de Pauli complet
- Utiliser la propagation de Pauli pour simuler les valeurs moyennes bruitées
- Observer comment l'augmentation du bruit des portes dégrade la précision du modèle quantique
Générer un réseau carré 3x3 et trouver une 4-coloration des arêtes
Les sommets du graphe représentent des qubits, et les arêtes représentent une connexion entre deux qubits. La coloration des arêtes correspond à des couches d'intrication uniques dans le Circuit quantique, de telle sorte que les portes sur des connexions associées à des couleurs différentes ne peuvent pas être appliquées simultanément.
Identifier un ensemble minimal de couches d'intrication uniques est souvent important pour mettre en œuvre des protocoles d'apprentissage du bruit efficaces, car le bruit de chaque couche doit être appris indépendamment. Plus nous devons apprendre de couches, plus nous avons besoin de shots sur le QPU. Pour cette démo, nous utilisons les informations sur les couches pour construire des Circuits bruités et injecter des instructions PauliLindbladError de qiskit-aer avant chaque couche d'intrication afin de modéliser le bruit des portes du QPU.
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "pauli_prop": "pauli-prop", "qiskit": "qiskit", "qiskit_addon_utils": "qiskit-addon-utils", "qiskit_aer": "qiskit-aer", "rustworkx": "rustworkx"}
_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 collections import defaultdict
import numpy as np
from qiskit.transpiler import CouplingMap
from qiskit_addon_utils.coloring import auto_color_edges
# Define rectangular square-lattice on 20 qubits
num_rows = 3
num_cols = 3
num_qubits = num_rows * num_cols
coupling_map = CouplingMap.from_grid(
num_rows=num_rows, num_columns=num_cols, bidirectional=False
)
# Create mapping from color to edge list
coloring = auto_color_edges(coupling_map.get_edges())
color_to_edge = defaultdict(list)
for edge, color in coloring.items():
color_to_edge[color].append(edge)
from rustworkx import PyDiGraph
from rustworkx.visualization import graphviz_draw
# Inspect graph coupling and unique entangling layers
print(
f"The circuit will have {num_qubits} qubits and {len(color_to_edge)} unique entangling layers."
)
sq_lattice = PyDiGraph()
sq_lattice.extend_from_weighted_edge_list(
[
(source, target, color)
for ((source, target), color) in coloring.items()
]
)
def color_edge_4color(edge):
color_dict = {0: "red", 1: "green", 2: "blue", 3: "orange"}
return {"color": color_dict[edge]}
graphviz_draw(sq_lattice, edge_attr_fn=color_edge_4color, method="neato")
The circuit will have 9 qubits and 4 unique entangling layers.

Générer des modèles de bruit synthétiques
Avant de créer les Circuits quantiques, nous générons un modèle de bruit (instance de PauliLindbladError) pour chacune des couches d'intrication. Nous les intégrerons plus tard comme instructions dans nos Circuits quantiques. Pour chaque couche, nous générons des canaux de bruit de différentes échelles. Plus précisément, nous générons des modèles de bruit avec une erreur par porte en couche (EPLG) d'environ .0004, .0008, .0012, .0016, et .002.
from qiskit.quantum_info import SparsePauliOp, pauli_basis
from qiskit_aer.noise import PauliLindbladError
# Pauli-Lindblad noise parameters
seed = 1764
target_EPLGs = [0.0004, 0.0008, 0.0012, 0.0016, 0.002]
def generate_random_pauli_lindblad_noise(
edges,
num_qubits: int | None = None,
noise_scale: float = 1e-3,
seed: int | None = None,
) -> PauliLindbladError:
"""Generate random Pauli-Lindblad noise over the full Pauli basis."""
if num_qubits is None:
num_qubits = np.max(edges)
basis_paulis = [p for p in pauli_basis(2) if np.sum(p.x + p.z)]
basis_paulis = SparsePauliOp.from_sparse_list(
[
(pauli.to_label(), edge, 1)
for pauli in basis_paulis
for edge in edges
],
num_qubits=num_qubits,
)
basis_paulis = basis_paulis.simplify()
basis_paulis = basis_paulis.paulis
rng = np.random.default_rng(seed=seed)
rates = rng.random(len(basis_paulis)) * noise_scale
return PauliLindbladError(generators=basis_paulis, rates=rates)
num_generators = (
(num_rows * num_cols)
+ (num_rows - 1) * num_cols
+ num_rows * (num_cols - 1)
)
noise_scales = [
EPLG * (num_rows * num_cols) / num_generators for EPLG in target_EPLGs
]
noise_models_per_EPLG = [
[
generate_random_pauli_lindblad_noise(
color_to_edge[color],
num_qubits=num_qubits,
noise_scale=noise_scale,
seed=seed,
)
for color in range(len(color_to_edge))
]
for noise_scale in noise_scales
]
Créer les Circuits quantiques
Pour cette démo, nous simulons la dynamique temporelle d'un modèle d'Ising à champ transverse (TFIM) pour un nombre croissant de pas de Trotter (1 à 10 pas). Pour chacune des 10 profondeurs de Circuit, nous simulons l'effet du bruit des portes, avec des modèles de bruit de différentes échelles (EPLGs = .0004, .0008, .0012, .0016, .002). Le bruit est inséré dans le QuantumCircuit sous forme d'instruction PauliLindbladError de Qiskit Aer. L'hamiltonien considéré est :
où décrit le couplage des spins plus proches voisins, , et est le champ transverse global.
Ici, nous implémentons l'hamiltonien évolué dans le temps à différentes échelles de temps et de bruit. Nous créons 60 Circuits au total : 10 Circuits sans bruit de profondeur de Trotter variable, et 50 Circuits bruités pour les 10 profondeurs de Trotter sur cinq échelles de bruit. Étant donné un graphe de connectivité, le modèle est paramétré par quelques variables :
num_steps: Le nombre de pas de TrotterJ: Force de couplage des sites connectésh: Intensité du champ magnétique externedt: Variation du temps au cours d'un pas de Trotterinitial_state_angle: Une excitation initiale, , à appliquer uniformément à tous les qubits
from typing import Any
from qiskit import QuantumCircuit
# Ising model parameters
num_steps = 10
J = -1.0
dt = 0.25 / abs(J)
h = 2.0 * abs(J)
initial_state_angle = np.pi / 18.0
rx_angle = 2.0 * h * dt
rzz_angle = 2.0 * J * dt
def generate_ising_circuit(
num_qubits: int,
num_steps: int,
rx_angle: float,
rzz_angle: float,
coloring: dict[Any, list[tuple[int, int]]],
layer_noise_models: list[PauliLindbladError] | None = None,
initial_state_angle: float | None = None,
) -> QuantumCircuit:
"""Generate a quantum circuit implementing a transverse-field Ising model"""
qc = QuantumCircuit(num_qubits)
if initial_state_angle:
qc.ry(initial_state_angle, range(num_qubits))
qc.rx(rx_angle / 2, range(num_qubits))
for i in range(num_steps):
for j, layer in enumerate(coloring):
edges = coloring[layer]
if layer_noise_models:
qc.append(layer_noise_models[j], qargs=range(num_qubits))
for edge in edges:
qc.rzz(rzz_angle, *edge)
if i == num_steps - 1:
qc.rx(rx_angle / 2, range(num_qubits))
else:
qc.rx(rx_angle, range(num_qubits))
return qc
# Create the noiseless and noisy circuits
noiseless_circs = []
noisy_circs = []
for steps in range(1, num_steps + 1):
noiseless_circs.append(
generate_ising_circuit(
num_qubits,
steps,
rx_angle,
rzz_angle,
color_to_edge,
initial_state_angle=initial_state_angle,
)
)
noisy_circs_per_step = []
for noise_models in noise_models_per_EPLG:
noisy_circs_per_step.append(
generate_ising_circuit(
num_qubits,
steps,
rx_angle,
rzz_angle,
color_to_edge,
layer_noise_models=noise_models,
initial_state_angle=initial_state_angle,
)
)
noisy_circs.append(noisy_circs_per_step)
print(
f"{num_steps} noiseless and {num_steps * len(target_EPLGs)} noisy Trotter circuits generated. {num_steps} different depths across {len(target_EPLGs)} different noise models"
)
print("\nBelow: Initial state and one noisy Trotter step.")
noisy_circs[0][0].draw("mpl", fold=-1)
10 noiseless and 50 noisy Trotter circuits generated. 10 different depths across 5 different noise models
Below: Initial state and one noisy Trotter step.

Spécifier l'observable et lancer les simulations
Pour cette démo, nous simulons les valeurs moyennes du corrélateur moyen à deux sites :
où correspond à une rotation uniforme sur tous les qubits, décrit couches de Trotter, et indexe toutes les paires de sommets connectés du réseau.
Enfin, nous utilisons pauli_prop pour simuler les valeurs moyennes de l'observable pour chacun des Circuits. Pour cette démo à neuf qubits, nous effectuons toutes les simulations de façon exacte. Aucune troncature de la propagation de Pauli ne sera effectuée ; par conséquent, les différences de valeurs moyennes entre les différents modèles de bruit peuvent être entièrement attribuées à l'erreur des portes. Le processus de simulation se déroule en quatre étapes :
- Faire évoluer les portes de Clifford du Circuit vers le début du Circuit avec
pauli_prop.evolve_through_cliffords - Propager l'observable à travers la partie non-Clifford du Circuit avec
pauli_prop.propagate_through_circuit- Nous effectuons des simulations exactes en laissant l'observable croître jusqu'à la taille de l'espace de Pauli complet,
- Propager l'observable évoluée à travers la partie de Clifford du Circuit avec
SparsePauliOp.evolvede Qiskit - Estimer la valeur moyenne par rapport à l'état zéro, , en sommant les coefficients de chaque terme diagonal de (termes contenant ou sur tous les qubits)
import time
from pauli_prop import evolve_through_cliffords, propagate_through_circuit
from qiskit.quantum_info import Pauli
# Average ZZ-correlator observable
id_pauli = Pauli("I" * num_qubits)
observable = 2 * SparsePauliOp(
[
id_pauli.dot(Pauli("ZZ"), [i, j])
for i in range(num_qubits)
for j in range(i + 1, num_qubits)
]
)
observable /= num_qubits**2
# Pauli propagation parameters
max_terms = 4**num_qubits # Exact propagation
atol = 1e-12
# Run simulations
exact_evs = []
noisy_evs = [[] for _ in range(len(target_EPLGs))]
st = time.perf_counter()
for i, noiseless_circ in enumerate(noiseless_circs):
cliff, non_cliff = evolve_through_cliffords(noiseless_circ)
evolved_obs = propagate_through_circuit(
observable, non_cliff, max_terms=max_terms, atol=atol, frame="h"
)[0]
evolved_obs.paulis = evolved_obs.paulis.evolve(cliff, frame="h")
exact_evs.append(
float(evolved_obs.coeffs[~evolved_obs.paulis.x.any(axis=1)].sum())
)
for j in range(len(target_EPLGs)):
noisy_circ = noisy_circs[i][j]
cliff, non_cliff = evolve_through_cliffords(noisy_circ)
evolved_obs = propagate_through_circuit(
observable, non_cliff, max_terms=max_terms, atol=1e-12, frame="h"
)[0]
evolved_obs.paulis = evolved_obs.paulis.evolve(cliff, frame="h")
noisy_evs[j].append(
float(evolved_obs.coeffs[~evolved_obs.paulis.x.any(axis=1)].sum())
)
print(
f"Ran {len(noiseless_circs)} noiseless and {len(target_EPLGs) * num_steps} noisy simulations in {int(time.perf_counter() - st)}s."
)
Ran 10 noiseless and 50 noisy simulations in 103s.
Observer l'effet de l'erreur des portes sur le modèle
N'oublie pas que, puisqu'il s'agit d'une expérience à neuf qubits, la routine de propagation de Pauli est exacte, et que toute l'erreur visible dans les graphiques de bruit peut être attribuée à l'erreur des portes.
import matplotlib.pyplot as plt
xs = range(1, num_steps + 1)
plt.plot(xs, exact_evs, label="Noiseless", color="black", marker="o")
colors = [".3", ".4", ".5", ".6", ".7"]
for i, evs in enumerate(noisy_evs):
plt.plot(
xs,
evs,
label=f"{target_EPLGs[i]} EPLG",
linestyle="--",
color=colors[i],
marker="o",
)
plt.xlabel("# Trotter steps")
plt.ylabel(r"$\langle Z_{tot}^2 \rangle$")
plt.legend()
plt.show()
