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?

PEC mit TREX

Probabilistic error cancellation (PEC) ist eine Technik zur Minderung von Quantenrauschen bei der Berechnung von Erwartungswerten von Observablen. Sie ermöglicht es dir, eine unverzerrte Schätzung der Erwartungswerte zu berechnen. Die Technik funktioniert, indem ein Ensemble verrauschter Circuits ausgeführt wird, in die das Inverse eines gelernten Rauschmodells eingespeist wurde. Das Rauschmodell wird dann mittels Pauli-Twirling (auch bekannt als randomized compiling) durch einen Pauli-Kanal approximiert. Solche Rauschmodelle lassen sich effizient lernen und mit PEC mitigieren (siehe Probabilistic error cancellation with sparse Pauli-Lindblad models on noisy quantum processors für weitere Details).

PEC

Twirled Readout Error eXtinction (TREX) ist eine Technik, die Messfehler mindert und nützlich ist, um Erwartungswerte von Pauli-Observablen abzuschätzen. Sie beruht auf Mess-Twirling (vor der Messung wird zufällig ein X-Gate eingefügt und das gemessene Ergebnis umgedreht), was die Übergangsmatrix der Auslesefehler diagonalisiert und leicht invertierbar macht. Ein Reskalierungsterm wird aus dem diagonalen Rauschkanal gelernt, indem getwirlte Kalibrierungs-Circuits gemessen werden, die im Nullzustand initialisiert wurden.

Measurement twirling

Diese Anleitung zeigt, wie du PEC verwendest, um Gate-Rauschen zu mindern, und es mit TREX kombinierst, um bei der Schätzung von Erwartungswerten von Observablen auch das Messrauschen zu mindern.

Der Workflow sieht so aus:

  1. Lege einen Circuit und eine Observable fest.
  2. Berechne exakte Erwartungswerte mit dem Statevector-Simulator von Qiskit.
  3. Berechne verrauschte, nicht mitigierte Erwartungswerte.
  4. Lerne das Rauschen der verschränkenden Schichten mit qiskit-noise-learning.
  5. Bereite ein PEC + TREX-Programm vor und füge ihm einen TREX-Kalibrierungs-Circuit hinzu.
  6. Vergleiche die mitigierten Erwartungswerte.

Lege einen Circuit und eine Observable fest​

Bereite einen Spiegel-Circuit einer Trotter-Zeitentwicklung eines linearen Ising-Modells im transversalen Feld auf acht Qubits vor. Spiegle den Circuit, um die Unitäre des Circuits aufzuheben und einen Identitäts-Circuit zu bilden. So kannst du den idealen Erwartungswert jeder Observablen leicht berechnen. Außerdem verdoppelt das Spiegeln die Circuit-Tiefe, was wiederum den Einfluss des Rauschens verdoppelt.

Verwende den Winkel θ=π2\theta= \frac{\pi}{2}, damit alle Gates im Circuit aus der Clifford-Gruppe stammen, was eine schnellere Simulation mit dem stabilizer-Simulator ermöglicht.

# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util

_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_aer": "qiskit-aer", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "qiskit_mitigation": "qiskit-mitigation", "qiskit_noise_learning": "qiskit-noise-learning", "samplomatic": "samplomatic"}
_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")
import warnings

import numpy as np
from qiskit.circuit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp

warnings.filterwarnings("ignore")

def construct_ising_circuit(
num_qubits: int,
num_trotter_steps: int,
rx_angle: float,
barrier: bool = True,
) -> QuantumCircuit:
circuit = QuantumCircuit(num_qubits)

for _step in range(num_trotter_steps):
circuit.rx(rx_angle, range(num_qubits))
if barrier:
circuit.barrier()
for first_qubit in (1, 2):
for idx in range(first_qubit, num_qubits, 2):
# equivalent to Rzz(-pi/2):
circuit.sdg([idx - 1, idx])
circuit.cz(idx - 1, idx)
if barrier:
circuit.barrier()

return circuit

def mirror_circuit(circuit: QuantumCircuit) -> QuantumCircuit:
mirror_circ = circuit.copy_empty_like()
mirror_circ.compose(circuit, inplace=True)
mirror_circ.barrier()
mirror_circ.compose(circuit.inverse(), inplace=True)
mirror_circ.measure_active()
return mirror_circ

# define observables
num_qubits = 8
observables = [
SparsePauliOp("I" * ((num_qubits - 1) - q) + "Z" + "I" * q)
for q in range(num_qubits)
]

# Instantiate circuit
num_trotter_steps = 5
rx_angle = np.pi / 2

circuit = construct_ising_circuit(
num_qubits, num_trotter_steps, rx_angle, barrier=False
)
mirrored_circuit = mirror_circuit(circuit)
print("Visualization of 1 mirrored Trotter step")
mirror_circuit(
construct_ising_circuit(num_qubits, 1, rx_angle, barrier=False)
).draw("mpl", scale=0.7)
Visualization of 1 mirrored Trotter step

Output of the previous code cell

Berechne verrauschte, nicht mitigierte Erwartungswerte​

Ein QuantumProgram enthält einen Quantum-Circuit und ein Samplex in einem Format, das für das Executor-Primitiv geeignet ist. MitigationTask.prepare erstellt aus dem Circuit, den Observablen und den Parameterwerten ein QuantumProgram und versieht den Circuit mit den angegebenen Boxing-Optionen in Boxen. Setze die Boxing-Option measure_annotations auf "change_basis", um die Messungen in die erforderlichen Basen zu drehen, und füge add_tags: "unique_box" hinzu, damit der Simulator ein Rauschmodell auf die Boxen des Circuits abbilden kann. Das Programm wird standardmäßig Pauli-getwirlt, was auf Hardware kohärentes Rauschen in das Pauli-Rauschen umwandelt, das die Mitigationsmethoden annehmen. num_randomizations legt fest, wie viele getwirlte Instanzen jedes Circuits gesampelt werden, und shots_per_randomization legt fest, wie viele Shots jede Instanz erhält; ihr Produkt ist also das Shot-Limit pro Circuit.

Wenn du QPU-Zugang hast, kannst du das Programm mit einem Executor ausführen, der aus einem Backend des QiskitRuntimeService erstellt wird. Dieses Beispiel verwendet einen AerSimulator, um das Programm lokal auszuführen. Seine Simulatoroption layer_noise_model speist auf jeder Schicht verschränkender Gates ein zufälliges Pauli-Lindblad-Rauschmodell ein, erzeugt mit random_pauli_lindblad_noise aus qiskit-mitigation mit einem Fehler pro Gate-Schicht (EPLG) von 0.003, typisch für aktuelle Heron-QPUs.

import matplotlib.pyplot as plt
from qiskit.circuit import CircuitInstruction
from qiskit.quantum_info import PauliLindbladMap, pauli_basis
from qiskit.transpiler import PassManager
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import Executor
from qiskit_mitigation import MitigationTask, find_combined_unique_layers
from qiskit_noise_learning.aer_executor.insert_noise_pass import (
InsertNoisePass,
)
from samplomatic import Tag
from samplomatic.utils import get_annotation

def generate_random_pauli_lindblad_noise(
layers,
num_qubits: int,
eplg: float = 3e-3,
readout_noise_scale: float = 3e-2,
seed: int | None = None,
) -> tuple[
list[tuple[CircuitInstruction, PauliLindbladMap]], PauliLindbladMap
]:
"""Generate random Pauli-Lindblad noise over the full Pauli basis."""
layer_noise_model = []
for layer in layers:
layer_instructions = layer.operation.params[0]
contain_measurement = any(
inst.name.startswith("measure") for inst in layer_instructions
)
if not contain_measurement:
edges = [
(inst.qubits[0]._index, inst.qubits[1]._index)
for inst in layer_instructions
] # extract the edges of the layer

basis_paulis = [p for p in pauli_basis(2) if np.sum(p.x + p.z)]
rng = np.random.default_rng(seed=seed)
num_generators = len(basis_paulis)
rates = rng.random(len(basis_paulis)) * eplg / num_generators

layer_noise = PauliLindbladMap.from_sparse_list(
[
(pauli.to_label(), edge, rate)
for pauli, rate in zip(basis_paulis, rates, strict=True)
for edge in edges
],
num_qubits=num_qubits,
)
layer_noise_model.append((layer, layer_noise))
# generate a readout noise model
rng = np.random.default_rng(seed=seed)
rates = rng.random(num_qubits) * readout_noise_scale
readout_noise_model = PauliLindbladMap.from_sparse_list(
[("X", [q], rate) for q, rate in enumerate(rates)],
num_qubits=num_qubits,
)
return layer_noise_model, readout_noise_model

def add_readout_noise(program, readout_noise_model):
"""Injects readout noise to the circuits in a quantum program."""
readout_noise_map = {}
for item in program.items:
readout_tag = item.circuit[-1].label[
-4:
] # extract the readout layer tag from the circuit
readout_noise_map[readout_tag] = readout_noise_model
item.circuit = PassManager(
[InsertNoisePass(noise_dict=readout_noise_map, noise_after=False)]
).run(item.circuit)

num_randomizations = 300
shots_per_randomization = 64

boxing_options = {"add_tags": "unique_box"}

# find the unique layers to generate the layered noise for
box_layers = find_combined_unique_layers(
[mirrored_circuit], custom_boxing_options=boxing_options
)
layer_noise_model, readout_noise_model = generate_random_pauli_lindblad_noise(
box_layers, num_qubits, seed=42
)

# a local noisy simulator in place of a QPU backend
noisy_executor = Executor(
AerSimulator(method="stabilizer"),
options={"simulator": {"layer_noise_model": layer_noise_model}},
)

task = MitigationTask()
program = task.prepare(
circuit=mirrored_circuit,
observables=observables,
parameters=None,
num_randomizations=num_randomizations,
shots_per_randomization=shots_per_randomization,
custom_boxing_options=boxing_options,
)
# For the simulation - we inject readout noise to the circuits of the program
add_readout_noise(program, readout_noise_model)

program._semantic_role = "simulation" # flag the program for the simulator
unmitigated_res = task.postprocess(noisy_executor.run(program).result())

x_vals = np.array(range(num_qubits))
fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(x_vals, [1] * len(x_vals), "k-", alpha=0.5, label="Exact")
ax.errorbar(
x_vals,
unmitigated_res.data.evs,
unmitigated_res.data.stds,
fmt="o",
label="Unmitigated",
)

x_tick_labels = [r"$Z_{ {i} }$" for i in range(num_qubits)]
ax.set_xlabel(r"$\theta/\pi$")
ax.set_ylabel("Expectation value")
ax.legend()
ax.set_xticks(x_vals, x_tick_labels)
plt.tight_layout()
plt.show()

Output of the previous code cell

Lerne das Rauschen der verschränkenden Schichten​

PEC beginnt damit, das getwirlte Rauschmodell jeder Schicht verschränkender Gates im Circuit zu lernen. Wenn du ein Markov-Rauschmodell annimmst, wird angenommen, dass die Tiefe des Circuits den Rauschkanal einer gegebenen verschränkenden Schicht nicht beeinflusst. Daher muss nur das Rauschmodell jeder einzigartigen Schicht verschränkender Gates gelernt werden. Der in dieser Anleitung verwendete Circuit lässt sich in zwei einzigartige Schichten aufteilen. Circuits so zu entwerfen, dass sie einen minimalen Satz einzigartiger verschränkender Schichten haben, ist wichtig, wenn du ein Rauschmodell lernen möchtest, da die einzigartigen Schichten unabhängig voneinander gelernt werden müssen. Wenn dein Circuit zu viele einzigartige Schichten hat, kann das Rauschlernen wegen der Sampling-Kosten für das Lernen vieler Schichten undurchführbar werden.

qiskit-noise-learning wird verwendet, um das Rauschen zu lernen. Der Lerner wiederholt jede Schicht fragment_depths-mal in seinen Lern-Circuits, twirlt jeden Circuit num_randomizations-mal und passt den Abfall der gemessenen Fidelitäten an ein Pauli-Lindblad-Modell an. Diese Parameter können die Genauigkeit des gelernten Modells beeinflussen. Je genauer das gelernte Rauschmodell ist, desto genauer wird die Rauschunterdrückung sein. Der folgende Code zeichnet das gelernte Rauschmodell, wobei die beiden Schichten in einer Karte zusammengeführt sind. Jedes Qubit ist ein Kreisdiagramm aus seinen über die beiden Schichten summierten Gewicht-1-Raten für X, Y und Z, und jede Kante zeigt die gelernten Gewicht-2-Raten. Eine Rate von 3e-4 bedeutet zum Beispiel, dass dieser Fehler bei etwa drei von 10.000 Anwendungen der Schicht auftritt.

Die Mitigation von Ausleserauschen erfolgt in der Nachverarbeitung mit TREX, im Gegensatz zum Gate-Rauschmodell, das für die Programmvorbereitung erforderlich ist, um das Anti-Rauschen einzuspeisen, das dem gelernten Rauschmodell entspricht. Daher kann das Ausleserauschen entweder beim Lernen des Gate-Rauschens ausgeführt und gelernt werden oder bei der Ausführung des mitigierten Programms. Das Ausleserauschen zusammen mit dem Gate-Rauschen zu lernen kann vorteilhaft sein, um ein in sich konsistentes Rauschmodell zu lernen (siehe zum Beispiel Disambiguating Pauli noise in quantum computers). Wenn Gate-Rauschen und Ausleserauschen im Rauschmodell getrennt sind, empfiehlt es sich, das Ausleserauschen bei der Ausführung des mitigierten Programms zu lernen, um Drift im Rauschen zwischen dem Zeitpunkt des Lernens und dem der Ausführung zu vermeiden.

In dieser Anleitung wird das Ausleserauschen während der Ausführung des mitigierten Programms mithilfe des TREX-Kalibrierungs-Circuits gelernt.

from qiskit.providers.fake_provider import GenericBackendV2
from qiskit.quantum_info import PauliList
from qiskit_ibm_runtime.results import LayerError, PauliLindbladError
from qiskit_ibm_runtime.visualization.embeddings import Embedding
from qiskit_mitigation import PEC
from qiskit_noise_learning.aer_executor import AerExecutor
from qiskit_noise_learning.noise_learner import LearningOptions, NoiseLearner
from samplomatic import InjectNoise

line_coupling = [(i, i + 1) for i in range(num_qubits - 1)]
backend = GenericBackendV2(
num_qubits=num_qubits,
basis_gates=["id", "rz", "sx", "x", "cz"],
coupling_map=line_coupling,
)

task_pec = PEC()
# first learn only the layers PEC will inject noise into, i.e. not the measurement layer
pec_layers = [
layer
for layer in task_pec.find_unique_layers(mirrored_circuit, boxing_options)
if get_annotation(layer.operation, InjectNoise)
]
layers = task_pec.find_unique_layers(mirrored_circuit, boxing_options)

learning_options = LearningOptions(
fragment_depths=[2, 4, 8, 16],
num_randomizations=250,
shots_per_randomizations=16,
)

# qiskit-noise-learning is not compatible with the Executor in local mode,
# and requires the simulator present in the package for running simulation
noise_dict = {
get_annotation(layer.operation, Tag).ref: noise_layer
for layer, noise_layer in layer_noise_model
}
noise_learner_simulator = AerExecutor(
qasm_simulator=AerSimulator(method="stabilizer"), noise_dict=noise_dict
)
learner = NoiseLearner(
backend, options=learning_options, executor=noise_learner_simulator
)
learned_noise = (
learner.run(pec_layers).result().to_dict()
) # keyed by InjectNoise reference

# Draw the learned model on the line, laid out as a line with the qubits in order.
# Composing the two layers' maps sums their rates, giving one map over all the edges.
embedding = Embedding(
[(0, 0), (0, 2), (0, 4), (2, 4), (4, 4), (4, 2), (4, 0), (2, 0)],
line_coupling,
)
first, second = learned_noise.values()
learned_model = first.compose(second).simplify()
error = PauliLindbladError(
PauliList(
learned_model.get_qubit_sparse_pauli_list_copy().to_pauli_list()
),
learned_model.rates,
)
fig = LayerError(circuit, list(range(num_qubits)), error).draw_map(
embedding, radius=0.4, height=460, width=620
)
fig.update_traces(
marker={
"cmin": 0,
"cmax": max(learned_model.rates),
"colorbar": {"tickformat": ".1e", "title": "rate"},
},
selector={"mode": "markers"},
)
fig.show()

Output of the previous code cell

Bereite ein kombiniertes PEC- und TREX-Programm vor​

Hier bereiten wir sowohl eine TREX- als auch eine PEC-Task vor und fügen die TREX-Task als Variable zur PEC-Task hinzu. Dadurch wird sichergestellt, dass alle für die TREX-Mitigation benötigten Eigenschaften (wie Mess-Twirling) dem PEC-mitigierten Element hinzugefügt werden. Das eingespeiste Rauschen wird einmal pro Randomisierung gesampelt, daher wird der statistische Fehler von PEC durch die Anzahl der Randomisierungen statt durch die Shots pro Randomisierung bestimmt. Deshalb werden viele Randomisierungen mit jeweils wenigen Shots verwendet, wobei die Gesamtzahl der Shots gleich bleibt wie beim nicht mitigierten Programm.

Der TREX-Kalibrierungs-Circuit wird dem Programm hinzugefügt, um das Ausleserauschen während der Ausführung des Programms zu lernen. Der TREX-Kalibrierungs-Circuit ist sehr flach, wodurch zusätzliche Randomisierungen dafür relativ günstig sind und das Ausleserauschen genauer gelernt werden kann.

from qiskit_mitigation import TREX

trex = TREX()

program_pec = task_pec.prepare(
circuit=mirrored_circuit,
observables=observables,
parameters=None,
num_randomizations=1200,
shots_per_randomization=16,
custom_boxing_options=boxing_options,
trex=trex,
noise_maps=learned_noise, # PEC option
noise_gain=0, # PEC option
max_sampling_overhead=100, # PEC option
)
program_pec = trex.prepare(
num_randomizations=1500,
quantum_program=program_pec,
custom_boxing_options=boxing_options,
)

# For the simulation - we inject readout noise to the circuits of the program
add_readout_noise(program_pec, readout_noise_model)

Untersuche den Shot-Overhead, führe das Programm aus und verarbeite die Ergebnisse nach​

PEC liefert eine unverzerrte Schätzung des Erwartungswerts, zum Preis eines großen Shot-Overheads. Der erforderliche Shot-Overhead lässt sich näherungsweise berechnen, indem man den Gamma-Faktor des Circuits bestimmt, der den Effekt des gelernten Rauschmodells auf den Circuit widerspiegelt. Standardmäßig wird die Anzahl der Randomisierungen der PEC-Task um den ungefähren Betrag des Shot-Overheads skaliert, der für eine unverzerrte Schätzung nötig ist (~γ2\gamma^2). Um diese automatische Skalierung zu vermeiden, kannst du den Parameter scale_randomizations_by_gamma auf False setzen. Der Shot-Overhead wird durch den Parameter max_sampling_overhead begrenzt.

Du solltest die Anzahl der Shots, die PEC benötigt, um eine unverzerrte Schätzung zu erreichen, vor der Ausführung prüfen, um zu entscheiden, ob dieser Aufwand tragbar ist, da sie exponentiell mit den Rauschraten und der Circuit-Tiefe skaliert.

print(f"The circuit calculated gamma is: {task_pec.gamma}")
print(f"The circuit shot overhead is ~{task_pec.gamma**2}")
The circuit calculated gamma is: 1.2774846896089953
The circuit shot overhead is ~1.631967132185391
program_pec._semantic_role = (
"simulation" # flag the program for the simulator
)
job_pec_results = noisy_executor.run(program_pec).result()

# post-processing
pec_res = task_pec.postprocess(job_pec_results)
# In order to post process the results without TREX, we call `postprocess` again, but instead of the default options we
# pass only the PEC item result and the `measure_noise_data` as `None`
pec_without_trex_res = task_pec.postprocess(
job_pec_results[task_pec._program_item_index], measure_noise_data=None
)

Vergleiche die mitigierten Erwartungswerte​

Zeichne abschließend den Erwartungswert jeder Observablen. PEC + TREX stellt die exakten Erwartungswerte innerhalb seiner Fehlerbalken wieder her, während das nicht mitigierte Ergebnis deutlich darunter verzerrt ist.

x = np.array(range(num_qubits))
x_tick_labels = [r"$Z_{ {i} }$" for i in range(num_qubits)]

fig, ax = plt.subplots(figsize=(8, 5))
ax.errorbar(
x,
unmitigated_res.data.evs,
unmitigated_res.data.stds,
fmt="o",
label="Twirl only",
)
ax.errorbar(
x,
pec_without_trex_res.data.evs,
pec_without_trex_res.data.stds,
fmt="o",
label="PEC only",
)
ax.errorbar(
x,
pec_res.data.evs,
pec_res.data.stds,
fmt="o",
label="PEC + TREX",
)

ax.plot(x, [1] * len(x), "--", color="black", alpha=0.5, label="Exact")

tick_labels = [f"$Z_{ {i} }$" for i in range(num_qubits)]
ax.set_xticks(x, tick_labels)

ax.set_ylabel("Expectation value")
ax.legend()
ax.set_title("PEC+TREX Mitigated Expectation Values")
plt.tight_layout()
plt.show()

Output of the previous code cell