Verrauschte Quantensysteme mit Pauli-Propagation simulieren
In diesem Leitfaden verwenden wir das Paket pauli-prop, um die Zeitdynamik eines verrauschten Ising-Modells mit transversalem Feld (TFIM) mit neun Qubits auf einem quadratischen 3x3-Gitter klassisch zu simulieren. Wir verwenden PauliLindbladError-Anweisungen, um einen Rauschkanal zu definieren, der auf eine Menge von Verschränkungsschichten wirkt. Anschließend propagieren wir die Observable rückwärts durch den verrauschten Circuit und schätzen Erwartungswerte für eine Reihe von Rauschmodellen sowie für den rauschfreien Fall.

Wenn die Observable rückwärts durch den Circuit propagiert wird, dämpft jeder Rauschkanal , der zur Verschränkungsschicht gehört, die Pauli-Terme in , die mit seinen Pauli-Lindblad-Generatoren antikommutieren. Genauer: Wenn ein Pauli-Generator von mit der Rate ist, dann transformiert sich ein Pauli-Term in wie folgt: , wobei der Koeffizient von ist. Sobald an den Anfang des Circuits propagiert wurde, lässt sich der Erwartungswert bezüglich des Nullzustands trivial berechnen, indem die Koeffizienten aller diagonalen Terme in summiert werden (Terme, die auf allen Qubits oder enthalten).
Arbeitsablauf:
- Das TFIM-Gitter festlegen und mithilfe einer Kantenfärbung eine minimale Menge von Verschränkungsschichten bestimmen
- Synthetische Rauschmodelle für jede eindeutige Verschränkungsschicht erzeugen
- Rauschmodelle in verschiedenen Größenordnungen erstellen, um den Einfluss des Gate-Rauschens auf das System zu untersuchen
- Rauschfreie und verrauschte Quantencircuits für die interessierenden Tiefen und Rauschstärken erstellen
- In verrauschten Circuits werden
PauliLindbladError-Anweisungen vor jeder Verschränkungsschicht eingefügt
- In verrauschten Circuits werden
- Mit Pauli-Propagation exakte Erwartungswerte des Systems bei verschiedenen Tiefen simulieren
- Für neun Qubits geschieht dies, indem auf Terme anwachsen darf und damit den gesamten Pauli-Raum abdeckt
- Mit Pauli-Propagation verrauschte Erwartungswerte simulieren
- Beobachten, wie zunehmendes Gate-Rauschen die Genauigkeit des Quantenmodells verschlechtert
Ein quadratisches 3x3-Gitter erzeugen und eine 4-Färbung der Kanten finden
Die Knoten im Graphen stehen für Qubits, und die Kanten stehen für eine Verbindung zwischen zwei Qubits. Die Kantenfärbung entspricht eindeutigen Verschränkungsschichten im Quantencircuit, sodass Gates auf Verbindungen mit unterschiedlichen Farben nicht gleichzeitig angewendet werden können.
Die Bestimmung einer minimalen Menge eindeutiger Verschränkungsschichten ist oft wichtig, um effiziente Protokolle zum Erlernen des Rauschens umzusetzen, da das Rauschen jeder Schicht unabhängig gelernt werden muss. Je mehr Schichten wir lernen müssen, desto mehr Shots müssen wir von der QPU nehmen. Für diese Demo verwenden wir die Schichtinformationen, um verrauschte Circuits aufzubauen und vor jeder Verschränkungsschicht PauliLindbladError-Anweisungen aus qiskit-aer einzufügen, um das Gate-Rauschen der QPU zu modellieren.
# 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.

Synthetische Rauschmodelle erzeugen
Bevor wir die Quantencircuits erstellen, erzeugen wir für jede der Verschränkungsschichten ein Rauschmodell (PauliLindbladError-Instanz). Wir werden sie später als Anweisungen in unsere Quantencircuits einbetten. Für jede Schicht erzeugen wir Rauschkanäle unterschiedlicher Stärke. Konkret erzeugen wir Rauschmodelle mit einem Error Per Layered Gate (EPLG) von etwa .0004, .0008, .0012, .0016, und .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
]
Die Quantencircuits erstellen
Für diese Demo simulieren wir die Zeitdynamik eines Ising-Modells mit transversalem Feld (TFIM) für zunehmende Anzahlen von Trotter-Schritten (1-10 Schritte). Für jede der 10 Circuit-Tiefen simulieren wir die Wirkung von Gate-Rauschen bei Rauschmodellen unterschiedlicher Stärke (EPLGs = .0004, .0008, .0012, .0016, .002). Das Rauschen wird als PauliLindbladError-Anweisung aus Qiskit Aer in das QuantumCircuit eingefügt. Der betrachtete Hamiltonian lautet:
wobei die Kopplung nächster benachbarter Spins beschreibt, , und das globale transversale Feld ist.
Hier implementieren wir den zeitentwickelten Hamiltonian über verschiedene Zeit- und Rauschskalen hinweg. Wir erstellen insgesamt 60 Circuits: 10 rauschfreie Circuits mit unterschiedlicher Trotter-Tiefe und 50 verrauschte Circuits für die 10 Trotter-Tiefen bei fünf Rauschstärken. Bei gegebenem Konnektivitätsgraphen wird das Modell durch einige Variablen parametrisiert:
num_steps: Die Anzahl der Trotter-SchritteJ: Kopplungsstärke verbundener Gitterplätzeh: Stärke des externen Magnetfeldsdt: Zeitänderung pro Trotter-Schrittinitial_state_angle: Eine anfängliche Anregung , die einheitlich auf alle Qubits angewendet wird
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.

Observable festlegen und Simulationen ausführen
Für diese Demo simulieren wir Erwartungswerte des durchschnittlichen Zwei-Platz-Korrelators:
wobei einer einheitlichen -Rotation auf allen Qubits entspricht, Trotter-Schichten beschreibt und alle verbundenen Knotenpaare auf dem Gitter indiziert.
Schließlich verwenden wir pauli_prop, um Erwartungswerte der Observable für jeden der Circuits zu simulieren. Für diese Demo mit neun Qubits führen wir alle Simulationen exakt durch. Es wird keine Pauli-Propagations-Abschneidung vorgenommen; daher lassen sich die Unterschiede der Erwartungswerte zwischen den verschiedenen Rauschmodellen vollständig auf den Gate-Fehler zurückführen. Der Simulationsprozess besteht aus vier Schritten:
- Die Clifford-Gates im Circuit mit
pauli_prop.evolve_through_cliffordsan den Anfang des Circuits entwickeln - Die Observable mit
pauli_prop.propagate_through_circuitdurch den Nicht-Clifford-Anteil des Circuits propagieren- Wir führen exakte Simulationen durch, indem wir die Observable auf die Größe des gesamten Pauli-Raums, , anwachsen lassen
- Die entwickelte Observable mit Qiskits
SparsePauliOp.evolvedurch den Clifford-Anteil des Circuits propagieren - Den Erwartungswert bezüglich des Nullzustands schätzen, indem die Koeffizienten aller diagonalen Terme in summiert werden (Terme, die auf allen Qubits oder 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."
)
Ran 10 noiseless and 50 noisy simulations in 103s.
Die Wirkung des Gate-Fehlers auf das Modell beobachten
Denke daran: Da es sich um ein Experiment mit neun Qubits handelt, ist die Pauli-Propagations-Routine exakt, und der gesamte Fehler in den Rauschdiagrammen lässt sich auf den Gate-Fehler zurückführen.
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()
