Zum Hauptinhalt springen
Dieses Projekt befindet sich in der Beta-Phase. Einen Fehler gefunden? GitHub Issue erstellen. Frage oder Idee? In Discussions fragen — wir freuen uns über dein Feedback!
Wie ist die Übersetzungsqualität auf dieser Seite?

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 Λ\Lambda zu definieren, der auf eine Menge von Verschränkungsschichten U\mathcal{U} wirkt. Anschließend propagieren wir die Observable OO rückwärts durch den verrauschten Circuit und schätzen Erwartungswerte für eine Reihe von Rauschmodellen sowie für den rauschfreien Fall.

Noisy EV

Wenn die Observable rückwärts durch den Circuit propagiert wird, dämpft jeder Rauschkanal Λk\Lambda_k, der zur Verschränkungsschicht Uk\mathcal{U}_k gehört, die Pauli-Terme in OO, die mit seinen Pauli-Lindblad-Generatoren antikommutieren. Genauer: 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,ifalls {P,Gk,i}=0c_P \mapsto c_P e^{-2\gamma_{k,i}} \quad \text{falls } \{P, G_{k,i}\}=0, wobei cPc_P der Koeffizient von PP ist. Sobald OO an den Anfang des Circuits propagiert wurde, lässt sich der Erwartungswert bezüglich des Nullzustands ∣0⟩⊗N|0\rangle^{\otimes N} trivial berechnen, indem die Koeffizienten aller diagonalen Terme in OO summiert werden (Terme, die auf allen Qubits ZZ oder II enthalten).

Arbeitsablauf:

  • Das TFIM-Gitter festlegen und mithilfe einer Kantenfärbung eine minimale Menge von Verschränkungsschichten bestimmen
  • Synthetische Rauschmodelle Λk\Lambda_k für jede eindeutige Verschränkungsschicht UkU_k 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
  • Mit Pauli-Propagation exakte Erwartungswerte des Systems bei verschiedenen Tiefen simulieren
    • Für neun Qubits geschieht dies, indem OO auf 494^9 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.

Output of the previous code cell

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:

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 nächster benachbarter Spins beschreibt, i<ji<j, und hh 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-Schritte
  • J: Kopplungsstärke verbundener Gitterplätze
  • h: Stärke des externen Magnetfelds
  • dt: Zeitänderung pro Trotter-Schritt
  • initial_state_angle: Eine anfängliche Anregung Ry(θ)R_y(\theta), 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.

Output of the previous code cell

Observable festlegen und Simulationen ausführen​

Für diese Demo simulieren wir Erwartungswerte des durchschnittlichen Zwei-Platz-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 einheitlichen Ry(θ)R_y(\theta)-Rotation auf allen 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, 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_cliffords an den Anfang des Circuits entwickeln
  • Die Observable mit pauli_prop.propagate_through_circuit durch 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, 494^9, anwachsen lassen
  • Die entwickelte Observable mit Qiskits SparsePauliOp.evolve durch den Clifford-Anteil des Circuits propagieren
  • Den Erwartungswert bezüglich des Nullzustands ∣0⟩⊗N|0\rangle^{\otimes N} schätzen, indem die Koeffizienten aller diagonalen Terme in OO summiert werden (Terme, die auf allen Qubits ZZ oder II 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()

Output of the previous code cell