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 ?

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, Λ\Lambda, agissant sur un ensemble de couches d'intrication, U\mathcal{U}. Nous propageons ensuite l'observable, OO, à rebours à travers le Circuit bruité et estimons les valeurs moyennes pour divers modèles de bruit, ainsi que pour le cas sans bruit.

Noisy EV

Lorsque l'observable est propagée à rebours à travers le Circuit, chaque canal de bruit, Λk\Lambda_k, associé à la couche d'intrication, Uk\mathcal{U}_k, amortit les termes de Pauli de OO qui anticommutent avec ses générateurs de Pauli-Lindblad. Plus précisément, si Gk,iG_{k,i} est un générateur de Pauli de Λk\Lambda_k de taux, γk,i\gamma_{k,i}, alors un terme de Pauli, PP, dans OO se transforme ainsi : cP↦cPe−2γk,isi {P,Gk,i}=0c_P \mapsto c_P e^{-2\gamma_{k,i}} \quad \text{si } \{P, G_{k,i}\}=0, où cPc_P est le coefficient de PP. Une fois que OO a été propagée jusqu'au début du Circuit, la valeur moyenne par rapport à l'état zéro, ∣0⟩⊗N|0\rangle^{\otimes N}, peut être calculée trivialement en sommant les coefficients de chaque terme diagonal de OO (termes contenant ZZ ou II 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, Λk\Lambda_k, pour chaque couche d'intrication unique, UkU_k
    • 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 PauliLindbladError sont insérées avant chaque couche d'intrication
  • 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 OO croître jusqu'à 494^9 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.

Output of the previous code cell

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 :

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 plus proches voisins, i<ji<j, et hh 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 Trotter
  • J : Force de couplage des sites connectés
  • h : Intensité du champ magnétique externe
  • dt : Variation du temps au cours d'un pas de Trotter
  • initial_state_angle : Une excitation initiale, Ry(θ)R_y(\theta), à 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.

Output of the previous code cell

Spécifier l'observable et lancer les simulations​

Pour cette démo, nous simulons les valeurs moyennes du corrélateur moyen à deux sites :

⟨O⟩=⟨Ztot2(s)⟩=1N2∑⟨Ψ(θ)∣(U†)sZjZk(U)s∣Ψ(θ)⟩\langle O \rangle = \langle Z_{tot}^2(s) \rangle = \frac{1}{N^2}\sum \langle \Psi(\theta)|(\mathscr{U}^{\dagger})^sZ_jZ_k(\mathscr{U})^s|\Psi(\theta) \rangle

où Ψ(θ)\Psi(\theta) correspond à une rotation uniforme Ry(θ)R_y(\theta) sur tous les qubits, Us\mathscr{U}^s décrit ss couches de Trotter, et (j,k)(j,k) 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, 494^9
  • Propager l'observable évoluée à travers la partie de Clifford du Circuit avec SparsePauliOp.evolve de Qiskit
  • Estimer la valeur moyenne par rapport à l'état zéro, ∣0⟩⊗N|0\rangle^{\otimes N}, en sommant les coefficients de chaque terme diagonal de OO (termes contenant ZZ ou II 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()

Output of the previous code cell