Skip to main content
IBM Quantum Platform

Simulation von verrauschten Quantensystemen mit Pauli-Ausbreitung

In dieser Anleitung verwenden wir das Paket pauli-prop , um die zeitliche Dynamik eines verrauschten Ising-Modells mit neun Qubits und transversalen Feldern (TFIM) auf einem quadratischen Gitter der Art „ 3x3 “ klassisch zu simulieren. Wir verwenden Anweisungen aus „ PauliLindbladError “, um einen Rauschkanal zu definieren ( Λ\Lambda ), der auf eine Reihe von Verschränkungsschichten wirkt ( U\mathcal{U} ). Anschließend leiten wir die Observable ( OO ) rückwärts durch den verrauschten Schaltkreis und schätzen die Erwartungswerte für eine Vielzahl von Rauschmodellen sowie für den rauschfreien Fall.

Lautes Elektroauto

Während die Beobachtungsgröße rückwärts durch den Schaltkreis weitergeleitet wird, dämpft jeder Rauschkanal Λk\Lambda_k, der mit einer Verschränkungsschicht Uk\mathcal{U}_k assoziiert ist, die Pauli-Terme in OO, die mit den zugehörigen Pauli-Lindblad-Generatoren antikommutieren. Konkret gilt: Wenn Gk,iG_{k,i} ein Pauli-Generator von Λk\Lambda_k mit der Rate γk,i\gamma_{k,i} ist, dann transformiert sich ein Pauli-Term PP in OO wie folgt: 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, wobei cPc_P der Koeffizient von PP ist. Sobald OO bis zum Anfang des Schaltkreises propagiert wurde, lässt sich der Erwartungswert in Bezug auf den Nullzustand ∣0⟩⊗N|0\rangle^{\otimes N} trivial berechnen, indem man die Koeffizienten jedes Diagonalterms in OO summiert (Terme, die ZZ oder II auf allen Qubits enthalten).

Workflows:

  • Legen Sie das TFIM-Gitter fest und verwenden Sie Kantenfärbung, um eine minimale Menge von Verschränkungsschichten zu identifizieren
  • Erzeuge synthetische Rauschmodelle, Λk\Lambda_k, für jede einzelne Verschränkungsschicht, UkU_k
    • Erstellen Sie Lärmmodelle verschiedener Größenordnungen, um die Auswirkungen von Gate-Rauschen auf das System zu untersuchen
  • Erstellen Sie rauschfreie und rauschbehaftete Quantenschaltungen für die verschiedenen Tiefen und Rauschskalen von Interesse
    • In rauschbehafteten Schaltungen werden vor jeder Verschränkungsschicht Befehle PauliLindbladError eingefügt
  • Verwenden Sie die Pauli-Ausbreitung, um die exakten Erwartungswerte des Systems in verschiedenen Tiefen zu simulieren
    • Bei neun Qubits geschieht dies, indem man OO auf 494^9 Terme anwachsen lässt, die den gesamten Pauli-Raum abdecken
  • Verwenden Sie die Pauli-Ausbreitung, um verrauschte Erwartungswerte zu simulieren
  • Beobachten Sie, wie zunehmendes Gate-Rauschen die Genauigkeit des Quantenmodells beeinträchtigt

Erzeuge ein quadratisches Gitter der Größe 3x3 und finde eine 4-Färbung der Kanten

Die Knoten im Graphen stellen Qubits dar, und die Kanten stellen eine Verbindung zwischen zwei Qubits dar. Die Färbung der Kanten entspricht eindeutigen Verschränkungsschichten im Quantenschaltkreis, sodass Gatter auf Verbindungen, die unterschiedlichen Farben zugeordnet sind, nicht gleichzeitig angewendet werden können.

Die Ermittlung einer minimalen Anzahl eindeutiger Verschränkungsschichten ist oft wichtig für die Implementierung effizienter Protokolle zum Lernen mit Rauschen, da das Rauschen für jede Schicht unabhängig gelernt werden muss. Je mehr Ebenen wir lernen müssen, desto mehr Aufnahmen müssen wir von der QPU machen. Für diese Demo nutzen wir die Schichteninformationen, um verrauschte Schaltkreise aufzubauen, und fügen vor qiskit-aer jeder Verschränkungsschicht „ PauliLindbladError “-Befehle ein, um das Gatterrauschen der QPU zu modellieren.

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

Synthetische Rauschmodelle generieren

Bevor wir die Quantenschaltungen erstellen, generieren wir für jede der Verschränkungsschichten ein Rauschmodell (eine Instanz von PauliLindbladError ). Wir werden sie später als Anweisungen in unsere Quantenschaltungen einbetten. Für jede Schicht erzeugen wir Rauschkanäle unterschiedlicher Größenordnung. Konkret erzeugen wir Rauschmodelle mit einem „Error Per Layered Gate“ (EPLG) von etwa und .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
]

Erstellen Sie die Quantenschaltungen

In dieser Demonstration simulieren wir die zeitliche Dynamik eines Ising-Modells mit Querfeld (TFIM) für eine steigende Anzahl von Trotter-Schritten (1–10 Schritte). Für jede der 10 Schaltungstiefen simulieren wir den Einfluss von Gate-Rauschen unter Verwendung von Rauschmodellen unterschiedlicher Größenordnung (EPLGs = .0004, .0008, .0012, .0016, .002). Das Rauschen wird als QuantumCircuit „ PauliLindbladError “-Anweisung aus Qiskit Aer in die Schaltung eingefügt. Der betrachtete Hamilton-Operator lautet:

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

wobei J>0J>0 die Kopplung zwischen den Spins der nächsten Nachbarn beschreibt, i<ji<j und hh das globale Transversalfeld ist.

Hier implementieren wir den zeitabhängigen Hamilton-Operator über verschiedene Zeit- und Rauschskalen hinweg. Wir erstellen insgesamt 60 Schaltkreise: 10 rauschfreie Schaltkreise mit unterschiedlicher Trotter-Tiefe und 50 rauschbehaftete Schaltkreise für die 10 Trotter-Tiefen über fünf Rauschskalen hinweg. Ausgehend von einem Konnektivitätsgraphen wird das Modell durch einige wenige Variablen parametrisiert:

  • num_steps: Die Anzahl der Trotter-Schritte
  • J: Kopplungsstärke verbundener Standorte
  • h: Stärke des externen Magnetfelds
  • dt: Zeitänderung über einen Trotter-Schritt hinweg
  • initial_state_angle: Eine anfängliche Anregung, Ry(θ)R_y(\theta), die gleichmäßig auf alle Qubits angewendet werden soll
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

Beobachtungsgrößen festlegen und Simulationen ausführen

Für diese Demonstration simulieren wir die Erwartungswerte des durchschnittlichen Zwei-Standort-Korrelators:

⟨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

wobei Ψ(θ)\Psi(\theta) einer gleichmäßigen Rotation Ry(θ)R_y(\theta) für alle Qubits entspricht, Us\mathscr{U}^s ss Trotter-Schichten beschreibt und (j,k)(j,k) alle verbundenen Knotenpaare auf dem Gitter indiziert.

Schließlich verwenden wir pauli_prop zur Simulation der beobachtbaren Erwartungswerte für jede der Schaltungen. Für diese 9-Qubit-Demonstration führen wir alle Simulationen exakt durch. Es wird keine Pauli-Propagationsabschneidung durchgeführt; daher lassen sich die Unterschiede in den Erwartungswerten zwischen den verschiedenen Rauschmodellen vollständig auf den Gate-Fehler zurückführen. Der Simulationsprozess erfolgt in vier Schritten:

  • Verlegen Sie die Clifford-Gatter in der Schaltung an den Anfang der Schaltung, indem Sie pauli_prop.evolve_through_cliffords
  • Führe die Beobachtungsgröße durch den Nicht-Clifford-Teil der Schaltung weiter, indem du pauli_prop.propagate_through_circuit
    • Wir führen exakte Simulationen durch, indem wir die Observable auf die Größe des gesamten Pauli-Raums anwachsen lassen: 494^9
  • Die entwickelte Observable über den Clifford-Teil der Schaltung weiterverarbeiten, unter Verwendung von Qiskits SparsePauliOp.evolve
  • Schätze den Erwartungswert in Bezug auf den Nullzustand, ∣0⟩⊗N|0\rangle^{\otimes N}, indem du die Koeffizienten jedes Diagonalterms in OO summiert (Terme, die ZZ oder II auf allen Qubits enthalten)
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.

Beobachten Sie den Einfluss des Gate-Fehlers auf das Modell

Beachten Sie, dass es sich hierbei um ein Experiment mit neun Qubits handelt, sodass die Pauli-Ausbreitungsroutine exakt ist und alle Fehler in den Rauschdiagrammen auf Gatterfehler zurückgeführt werden können.

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
War diese Seite hilfreich?
Melden Sie auf GitHub einen Fehler, einen Tippfehler oder fordern Sie Inhalte an.