Skip to main content
IBM Quantum Platform

Simulation de systèmes quantiques soumis à du bruit à l'aide de la propagation de Pauli

Dans ce guide, nous utilisons le package pauli-prop pour simuler de manière classique la dynamique temporelle d'un modèle d'Ising à champ transversal (TFIM) à neuf qubits avec bruit, sur un réseau carré de 3x3. Nous utilisons les instructions « PauliLindbladError » pour définir un canal de bruit, Λ\Lambda, agissant sur un ensemble de couches d'intrication, U\mathcal{U}. Nous faisons ensuite remonter l'observable, OO, à travers le circuit bruité et estimons les valeurs attendues pour divers modèles de bruit, ainsi que pour le cas sans bruit.

Véhicule électrique bruyant

Au fur et à mesure que l'observable se propage en sens inverse à travers le circuit, chaque canal de bruit, Λk\Lambda_k, associé à une couche d'intrication, Uk\mathcal{U}_k, atténue les termes de Pauli dans 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 avec un taux γk,i\gamma_{k,i}, alors un terme de Pauli, PP, dans OO se transforme comme suit : cP↦cPe−2γk,iif {P,Gk,i}=0c_P \mapsto c_P e^{-2\gamma_{k,i}} \quad \text{if } \{P, G_{k,i}\}=0, où cPc_P est le coefficient de PP. Une fois que OO a été propagé jusqu’au début du circuit, la valeur attendue par rapport à l’état zéro, ∣0⟩⊗N|0\rangle^{\otimes N}, peut être calculée de manière triviale en additionnant les coefficients de chaque terme diagonal dans OO (termes contenant ZZ ou II sur tous les qubits).

Flux de travail :

  • Spécifier le réseau TFIM et utiliser la coloration des arêtes pour identifier un ensemble minimal de couches d'intrication
  • Générer des modèles de bruit synthétique, Λk\Lambda_k, pour chaque couche d'intrication distincte, UkU_k
    • Créer des modèles de bruit à différentes échelles afin d'étudier l'impact du bruit de porte sur le système
  • Créer des circuits quantiques sans bruit et avec bruit pour les différentes profondeurs et échelles de bruit d'intérêt
    • Dans les circuits bruyants, des instructions PauliLindbladError sont insérées avant chaque couche d'intrication
  • Utiliser la propagation de Pauli pour simuler les valeurs exactes des espérances du système à différentes profondeurs
    • Pour neuf qubits, cela s'effectue en faisant évoluer l' OO e vers des termes de type 494^9, couvrant ainsi l'intégralité de l'espace de Pauli
  • Utiliser la propagation de Pauli pour simuler des valeurs attendues bruitées
  • Observez comment l'augmentation du bruit de la porte dégrade la précision du modèle quantique

Générer un réseau carré de 3x3 s et trouver une coloration à 4 couleurs sur les 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 distinctes dans le circuit quantique, de sorte que les portes situées sur des connexions associées à des couleurs différentes ne peuvent pas être appliquées simultanément.

Il est souvent important d'identifier un ensemble minimal de couches d'intrication uniques pour mettre en œuvre des protocoles d'apprentissage du bruit efficaces, car le bruit de chaque couche doit être appris indépendamment. Plus il y a de couches à apprendre, plus il faut effectuer de prises de vue à partir du QPU. Pour cette démonstration, nous utilisons les informations relatives aux couches pour construire des circuits bruités et injecter des instructions « PauliLindbladError » avant qiskit-aer chaque couche d’intrication afin de modéliser le bruit des portes du QPU.

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")

Output:

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étique

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 ultérieurement dans nos circuits quantiques sous forme d'instructions. Pour chaque couche, nous générons des canaux de bruit à différentes échelles. Plus précisément, nous générons des modèles de bruit avec un indicateur d’erreur par porte en couches (EPLG) d’environ et .0004, .0008, .0012, .0016, .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

Dans cette démonstration, nous simulons la dynamique temporelle d'un modèle d'Ising à champ transversal (TFIM) pour un nombre croissant d'étapes de Trotter (de 1 à 10 étapes). Pour chacune des 10 profondeurs de circuit, nous simulons l'effet du bruit de grille, à partir de modèles de bruit à différentes échelles (EPLGs = .0004, .0008, .0012, .0016, .002). Le bruit est injecté dans le circuit QuantumCircuit sous la forme d'une instruction « PauliLindbladError » issue de Qiskit Aer. L'hamiltonien considéré est le suivant :

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 entre les spins voisins les plus proches, i<ji<j, et hh représente le champ transversal global.

Nous mettons ici en œuvre l'hamiltonien évolué dans le temps à différentes échelles de temps et de bruit. Nous créons au total 60 circuits : 10 circuits silencieux dont la profondeur de Trotter varie, et 50 circuits bruyants correspondant aux 10 profondeurs de Trotter, répartis sur cinq échelles de bruit. Pour un graphe de connectivité donné, 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 sur une pas de Trotter
  • initial_state_angle: Une excitation initiale, Ry(θ)R_y(\theta), à appliquer de manière uniforme à 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)

Output:

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 les observables et lancer les simulations

Dans cette démonstration, nous simulons les valeurs attendues 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 de type « Ry(θ)R_y(\theta) » appliquée à tous les qubits, Us\mathscr{U}^s décrit ss couches de Trotter, et (j,k)(j,k) répertorie toutes les paires de sommets connectés sur le réseau.

Enfin, nous utilisons pauli_prop pour simuler les valeurs attendues observables pour chacun des circuits. Pour cette démonstration à neuf qubits, nous effectuons toutes les simulations de manière exacte. Aucune troncature de propagation de Pauli ne sera effectuée; par conséquent, les différences entre les valeurs attendues selon les différents modèles de bruit peuvent être entièrement attribuées à l'erreur de la porte. Le processus de simulation se déroule en quatre étapes :

  • Déplacez les portes de Clifford du circuit vers l'avant de celui-ci à l'aide de pauli_prop.evolve_through_cliffords
  • Propager l'observable à travers la partie non-Clifford du circuit à l'aide de pauli_prop.propagate_through_circuit
    • Nous réalisons des simulations exactes en laissant l'observable s'étendre jusqu'à couvrir l'intégralité de l'espace de Pauli, 494^9
  • Propager l'observable évoluée à travers la partie Clifford du circuit à l'aide de Qiskit’s SparsePauliOp.evolve
  • Estimer la valeur attendue par rapport à l'état zéro, ∣0⟩⊗N|0\rangle^{\otimes N}, en additionnant 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."
)

Output:

Ran 10 noiseless and 50 noisy simulations in 103s.

Observer l'effet de l'erreur de grille sur le modèle

N'oubliez pas que, puisqu'il s'agit d'une expérience à neuf qubits, la routine de propagation de Pauli est exacte, et que toutes les erreurs observées dans les graphiques de bruit peuvent être attribuées à des erreurs de porte.

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:

Output of the previous code cell
Cette page a-t-elle été utile ?
Signaler un bogue, une coquille ou proposer du contenu sur GitHub.