PEC com TREX
Cancelamento probabilístico de erro (PEC) é uma técnica para mitigar o ruído quântico no cálculo de valores esperados de observáveis. Ela permite calcular uma estimativa não enviesada dos valores esperados. Essa técnica funciona executando um conjunto de circuitos ruidosos injetados com o inverso de um modelo de ruído aprendido. O modelo de ruído é então aproximado por um canal de Pauli usando Pauli twirling (também conhecido como compilação randomizada). Esses modelos de ruído podem ser aprendidos com eficiência e mitigados usando PEC (veja Probabilistic error cancellation with sparse Pauli-Lindblad models on noisy quantum processors para mais detalhes).
Twirled Readout Error eXtinction (TREX) é uma técnica que mitiga erros de medição e é útil para estimar valores esperados de observáveis de Pauli. Ela se baseia em twirling de medição (injetar aleatoriamente uma porta X antes da medição e inverter o resultado medido), o que diagonaliza a matriz de transferência do erro de leitura e facilita sua inversão. Um termo de reescalonamento é aprendido a partir do canal de ruído diagonal medindo circuitos de calibração com twirling inicializados no estado zero.
Este guia mostra como usar o PEC para mitigar o ruído das portas e combiná-lo com o TREX para também mitigar o ruído de medição ao estimar valores esperados de observáveis.
O fluxo de trabalho é o seguinte:
- Especifique um circuito e um observável.
- Calcule valores esperados exatos usando o simulador de vetor de estado do Qiskit.
- Calcule valores esperados ruidosos, sem mitigação.
- Aprenda o ruído das camadas de emaranhamento usando qiskit-noise-learning.
- Prepare um programa PEC + TREX e adicione a ele um circuito de calibração TREX.
- Compare os valores esperados mitigados.
Especifique um circuito e um observável
Prepare um circuito espelho de uma evolução temporal trotterizada de um modelo de Ising linear de campo transverso em oito qubits. Espelhe o circuito para cancelar a unitária do circuito e formar um circuito identidade. Isso permite calcular facilmente o valor esperado ideal de cada observável. Além disso, o espelhamento dobra a profundidade do circuito, o que por sua vez dobra o efeito do ruído.
Use o ângulo para manter todas as portas do circuito no grupo de Clifford, permitindo uma simulação mais rápida com o simulador stabilizer.
# 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
Calcule valores esperados ruidosos, sem mitigação
Um QuantumProgram contém um circuito quântico e um samplex em um formato adequado para a primitiva Executor. MitigationTask.prepare cria um QuantumProgram a partir do circuito, dos observáveis e dos valores dos parâmetros, e envolve o circuito em caixas com as opções de boxing fornecidas. Defina a opção de boxing measure_annotations como "change_basis" para rotacionar as medições para as bases necessárias, e adicione add_tags: "unique_box" para que o simulador possa mapear um modelo de ruído nas caixas do circuito. O programa recebe Pauli twirling por padrão, o que em hardware transforma o ruído coerente no ruído de Pauli que os métodos de mitigação assumem. num_randomizations define quantas instâncias com twirling de cada circuito são amostradas e shots_per_randomization define quantos shots cada instância recebe, de modo que o produto dos dois é o limite de shots por circuito.
Se você tem acesso a QPU, pode executar o programa com um Executor criado a partir de um backend do QiskitRuntimeService. Este exemplo usa um AerSimulator para executar o programa localmente. Sua opção de simulador layer_noise_model injeta um modelo de ruído de Pauli-Lindblad aleatório em cada camada de portas de emaranhamento, gerado com random_pauli_lindblad_noise de qiskit-mitigation com um erro por porta em camada (EPLG) de 0.003, típico dos QPUs Heron atuais.
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()
Aprenda o ruído das camadas de emaranhamento
O PEC começa aprendendo o modelo de ruído com twirling de cada camada de portas de emaranhamento no circuito. Se você assumir um modelo de ruído markoviano, assume-se que a profundidade do circuito não afeta o canal de ruído de uma determinada camada de emaranhamento. Portanto, só o modelo de ruído de cada camada única de portas de emaranhamento precisa ser aprendido. O circuito usado neste guia pode ser dividido em duas camadas únicas. Projetar circuitos com um conjunto mínimo de camadas de emaranhamento únicas é importante se você quer aprender um modelo de ruído, já que as camadas únicas precisam ser aprendidas de forma independente. Se o seu circuito tiver camadas únicas demais, o aprendizado de ruído pode se tornar inviável devido ao custo de amostragem de aprender muitas camadas.
qiskit-noise-learning é usado para aprender o ruído. O aprendiz repete cada camada fragment_depths vezes em seus circuitos de aprendizado, aplica twirling a cada circuito num_randomizations vezes e ajusta o decaimento das fidelidades medidas a um modelo de Pauli-Lindblad. Esses parâmetros podem afetar a precisão do modelo aprendido. Quanto mais preciso for o modelo de ruído aprendido, mais precisa será a anulação do ruído.
O código a seguir desenha o modelo de ruído aprendido com as duas camadas combinadas em um único mapa. Cada qubit é um gráfico de pizza das suas taxas X, Y e Z de peso 1 somadas nas duas camadas, e cada aresta mostra as taxas de peso 2 aprendidas. Por exemplo, uma taxa de 3e-4 significa que esse erro ocorre em aproximadamente três de cada 10.000 usos da camada.
A mitigação do ruído de leitura é feita no pós-processamento usando o TREX, ao contrário do modelo de ruído das portas, que é necessário para a preparação do programa a fim de injetar o anti-ruído correspondente ao modelo de ruído aprendido. Portanto, o ruído de leitura pode ser executado e aprendido junto com o ruído das portas, ou ao executar o programa mitigado. Aprender o ruído de leitura junto com o ruído das portas pode ser vantajoso para aprender um modelo de ruído autoconsistente (veja Disambiguating Pauli noise in quantum computers, por exemplo). Nos casos em que o ruído das portas e o ruído de leitura são separados no modelo de ruído, recomenda-se aprender o ruído de leitura ao executar o programa mitigado, para evitar deriva do ruído entre o momento em que ele foi aprendido e o momento em que foi executado.
Neste guia, o ruído de leitura é aprendido durante a execução do programa mitigado, usando o circuito de calibração TREX.
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()
Prepare um programa combinado de PEC e TREX
Aqui preparamos uma tarefa TREX e uma tarefa PEC, e adicionamos a tarefa TREX como variável à tarefa PEC. Isso garante que todas as propriedades necessárias para a mitigação TREX sejam adicionadas ao item mitigado pelo PEC (como o twirling de medição). O ruído injetado é amostrado uma vez por randomização, então o erro estatístico do PEC é controlado pelo número de randomizações, e não pelos shots por randomização. Por isso, usamos muitas randomizações - cada uma com poucos shots - mantendo o número total de shots igual ao do programa sem mitigação.
O circuito de calibração TREX é adicionado ao programa para aprender o ruído de leitura durante a execução do programa. O circuito de calibração TREX é muito raso, o que torna relativamente barato usar randomizações adicionais para ele e permite que o ruído de leitura seja aprendido com mais precisão.
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)
Examine o overhead de shots, execute o programa e faça o pós-processamento
O PEC retorna uma estimativa não enviesada do valor esperado, ao custo de um grande overhead de shots. O overhead de shots necessário pode ser aproximado calculando o fator gama do circuito, que reflete o efeito do modelo de ruído aprendido sobre o circuito. Por padrão, o número de randomizações da tarefa PEC é escalado pela quantidade aproximada de overhead de shots necessária para ter uma estimativa não enviesada (~). Para evitar esse escalonamento automático, você pode definir o parâmetro scale_randomizations_by_gamma como False. O overhead de shots é limitado pelo parâmetro max_sampling_overhead.
Você deve revisar o número de shots que o PEC exige para chegar a uma estimativa não enviesada antes da execução, para decidir se essa quantidade é viável, pois ela cresce exponencialmente com as taxas de ruído e a profundidade do circuito.
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
)
Compare os valores esperados mitigados
Por fim, plote o valor esperado de cada observável. O PEC + TREX recupera os valores esperados exatos dentro de suas barras de erro, enquanto o resultado sem mitigação fica bem abaixo deles.
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()