Simula sistemas cuánticos con ruido mediante propagación de Pauli
En esta guía usamos el paquete pauli-prop para simular de forma clásica la dinámica temporal de un modelo de Ising de campo transversal (TFIM) de nueve qubits con ruido en una red cuadrada de 3x3. Usamos instrucciones PauliLindbladError para definir un canal de ruido, , que actúa sobre un conjunto de capas entrelazantes, . Luego propagamos el observable, , hacia atrás a través del circuito con ruido y estimamos valores esperados para diversos modelos de ruido, además del caso sin ruido.

A medida que el observable se propaga hacia atrás a través del circuito, cada canal de ruido, , asociado con la capa entrelazante, , amortigua los términos de Pauli de que anticonmutan con sus generadores de Pauli-Lindblad. En concreto, si es un generador de Pauli de con tasa , entonces un término de Pauli, , de se transforma así: , donde es el coeficiente de . Una vez que se ha propagado hasta el inicio del circuito, el valor esperado respecto al estado cero, , puede calcularse trivialmente sumando los coeficientes de cada término diagonal de (términos que contienen o en todos los qubits).
Flujo de trabajo:
- Especificar la red del TFIM y usar la coloración de aristas para identificar un conjunto mínimo de capas entrelazantes
- Generar modelos de ruido sintéticos, , para cada capa entrelazante única,
- Crear modelos de ruido de varias escalas para estudiar el impacto del ruido de las puertas en el sistema
- Crear circuitos cuánticos sin ruido y con ruido para las distintas profundidades y escalas de ruido de interés
- En los circuitos con ruido, se insertan instrucciones
PauliLindbladErrorantes de cada capa entrelazante
- En los circuitos con ruido, se insertan instrucciones
- Usar la propagación de Pauli para simular valores esperados exactos del sistema a distintas profundidades
- Para nueve qubits, esto se hace dejando que crezca hasta términos, cubriendo todo el espacio de Pauli
- Usar la propagación de Pauli para simular valores esperados con ruido
- Observar cómo el aumento del ruido de las puertas degrada la precisión del modelo cuántico
Genera una red cuadrada de 3x3 y encuentra una 4-coloración de las aristas
Los vértices del grafo representan qubits, y las aristas representan una conexión entre dos qubits. La coloración de aristas corresponde a capas entrelazantes únicas en el circuito cuántico, de modo que las puertas en conexiones asociadas con colores distintos no pueden aplicarse simultáneamente.
Identificar un conjunto mínimo de capas entrelazantes únicas suele ser importante para implementar protocolos eficientes de aprendizaje de ruido, ya que el ruido de cada capa debe aprenderse de forma independiente. Cuantas más capas debamos aprender, más shots necesitamos tomar de la QPU. Para esta demostración, usamos la información de las capas para construir circuitos con ruido e inyectar instrucciones PauliLindbladError de qiskit-aer antes de cada capa entrelazante para modelar el ruido de las puertas de la 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.

Genera modelos de ruido sintéticos
Antes de crear los circuitos cuánticos, generamos un modelo de ruido (instancia de PauliLindbladError) para cada una de las capas entrelazantes. Más adelante los incrustaremos como instrucciones en nuestros circuitos cuánticos. Para cada capa, generamos canales de ruido de distintas escalas. En concreto, generamos modelos de ruido con un Error Per Layered Gate (EPLG) de aproximadamente .0004, .0008, .0012, .0016, y .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
]
Crea los circuitos cuánticos
Para esta demostración, simulamos la dinámica temporal de un modelo de Ising de campo transversal (TFIM) con números crecientes de pasos de Trotter (de 1 a 10 pasos). Para cada una de las 10 profundidades de circuito, simulamos el efecto del ruido de las puertas, dados modelos de ruido de distintas escalas (EPLGs = .0004, .0008, .0012, .0016, .002). El ruido se inserta en el QuantumCircuit como una instrucción PauliLindbladError de Qiskit Aer. El hamiltoniano considerado es:
donde describe el acoplamiento de espines vecinos más cercanos, , y es el campo transversal global.
Aquí implementamos el hamiltoniano evolucionado en el tiempo a lo largo de varias escalas de tiempo y de ruido. Creamos 60 circuitos en total: 10 circuitos sin ruido que varían en profundidad de Trotter, y 50 circuitos con ruido para las 10 profundidades de Trotter en cinco escalas de ruido. Dado un grafo de conectividad, el modelo está parametrizado por unas pocas variables:
num_steps: El número de pasos de TrotterJ: Intensidad de acoplamiento de los sitios conectadosh: Intensidad del campo magnético externodt: Cambio de tiempo a lo largo de un paso de Trotterinitial_state_angle: Una excitación inicial, , que se aplica de manera uniforme a todos los 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.

Especifica el observable y ejecuta las simulaciones
Para esta demostración, simulamos valores esperados del correlador promedio de dos sitios:
donde corresponde a una rotación uniforme en todos los qubits, describe capas de Trotter, y indexa todos los pares de vértices conectados de la red.
Por último, usamos pauli_prop para simular los valores esperados del observable para cada uno de los circuitos. Para esta demostración de nueve qubits, realizamos todas las simulaciones de forma exacta. No se realizará ningún truncamiento de la propagación de Pauli; por lo tanto, las diferencias en los valores esperados entre los distintos modelos de ruido pueden atribuirse por completo al error de las puertas. El proceso de simulación se divide en cuatro pasos:
- Evolucionar las puertas Clifford del circuito hasta el principio del circuito usando
pauli_prop.evolve_through_cliffords - Propagar el observable a través de la parte no Clifford del circuito usando
pauli_prop.propagate_through_circuit- Realizamos simulaciones exactas permitiendo que el observable crezca hasta el tamaño de todo el espacio de Pauli,
- Propagar el observable evolucionado a través de la parte Clifford del circuito usando
SparsePauliOp.evolvede Qiskit - Estimar el valor esperado respecto al estado cero, , sumando los coeficientes de cada término diagonal de (términos que contienen o en todos los 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.
Observa el efecto del error de las puertas en el modelo
Recuerda que, como este es un experimento de nueve qubits, la rutina de propagación de Pauli es exacta, y todo el error en los gráficos de ruido puede atribuirse al error de las puertas.
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()
