PEC com SLC
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).
O PEC exige um overhead exponencial de shots para calcular uma estimativa não enviesada. Shaded Lightcones (SLC) é uma técnica que usa recursos clássicos para limitar a sensibilidade a erros ao longo do circuito. Com esses limites, o usuário pode ajustar de forma controlada o viés na estimativa do observável.
Este guia mostra como usar o PEC combinado com o SLC para mitigar o ruído das portas com overhead de shots reduzido em comparação com o PEC simples.
O fluxo de trabalho é o seguinte:
- Especifique um circuito e um observável.
- Calcule valores esperados ruidosos, sem mitigação.
- Calcule limites direto e inverso para o viés.
- Aprenda o ruído das camadas de emaranhamento usando qiskit-noise-learning.
- Use o ruído aprendido para combinar os limites calculados em uma matriz de escalas locais.
- Escolha o limite de viés desejado com base no compromisso entre overhead de shots e viés.
- Prepare um programa PEC + SLC, execute-o e faça o pós-processamento.
- Compare os valores esperados mitigados e o overhead de shots.
Especifique um circuito e um observável
Prepare um circuito espelho de uma evolução temporal trotterizada de um Hamiltoniano de cadeia de Ising 1D em oito qubits. Espelhe o circuito para cancelar sua unitária e formar um circuito identidade, de modo que o valor esperado ideal de cada observável seja 1, sem precisar calculá-lo. 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 tornar a dinâmica não Clifford.
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_slc": "qiskit-addon-slc", "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
from qiskit_mitigation.utils import get_measurement_bases
warnings.filterwarnings("ignore")
def construct_ising_circuit(
num_qubits: int,
num_trotter_steps: int,
rx_angle: float,
) -> QuantumCircuit:
circuit = QuantumCircuit(num_qubits)
for _ in range(num_trotter_steps):
circuit.rx(rx_angle, range(num_qubits))
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)
return circuit
def prepare_basis(
circuit: QuantumCircuit, basis: list[int]
) -> QuantumCircuit:
# basis is a list of integer values from 0 to 3. These map to the basis measurement as:
# 0 = I; 1 = Z; 2 = X; 3 = Y
assert len(basis) == circuit.num_qubits
out_circ = circuit.copy_empty_like()
for qb, bas in enumerate(basis):
if bas == 2:
out_circ.h(qb)
elif bas == 3:
out_circ.rx(-np.pi / 2, qb)
out_circ.barrier()
out_circ.compose(circuit, inplace=True)
return out_circ
def mirror_circuit(circuit: QuantumCircuit) -> QuantumCircuit:
mirror_circ = circuit.copy_empty_like()
mirror_circ.compose(circuit.inverse(), inplace=True)
mirror_circ.barrier()
mirror_circ.compose(circuit, inplace=True)
mirror_circ.measure_active()
return mirror_circ
# define observables
num_qubits = 8 # Width of the Ising chain
# The observable to measure: one X component (on qubit 2) and one Z component (on qubit 5)
target_obs_sparse = [("XZ", [2, 5], 1.0)]
observable = SparsePauliOp.from_sparse_list(
target_obs_sparse, num_qubits=num_qubits
)
# Determine the measurement basis each qubit requires for this observable.
bases, _ = get_measurement_bases(observable)
# Instantiate the mirrored circuit and prepend the basis-preparation gates
num_trotter_steps = 6
rx_angle = np.pi / 4
circuit = construct_ising_circuit(num_qubits, num_trotter_steps, rx_angle)
mirrored_circuit = mirror_circuit(circuit)
mirrored_circuit = prepare_basis(mirrored_circuit, bases[0])
print("Visualization of 1 mirrored Trotter step")
prepare_basis(
mirror_circuit(construct_ising_circuit(num_qubits, 1, rx_angle)), bases[0]
).draw("mpl", scale=0.6, fold=-1)
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 ruído de um modelo de ruído de Pauli-Lindblad dado em cada camada de portas de emaranhamento. Aqui geramos um modelo de ruído de Pauli-Lindblad aleatório com um erro por porta em camada (EPLG) de 0.01, quase uma ordem de grandeza acima dos QPUs Heron atuais típicos, para ampliar o efeito do uso do SLC também em um problema de pequena escala.
from qiskit.circuit import CircuitInstruction
from qiskit.quantum_info import PauliLindbladMap, pauli_basis
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import Executor
from qiskit_mitigation import MitigationTask, find_combined_unique_layers
from samplomatic import Tag
from samplomatic.utils import get_annotation
def generate_random_pauli_lindblad_noise(
layers,
num_qubits: int,
eplg: float = 3e-3,
seed: int | None = None,
) -> list[tuple[CircuitInstruction, 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 contain_measurement: # do not add measurement noise
layer_noise = PauliLindbladMap.identity(num_qubits)
else:
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))
return layer_noise_model
num_randomizations = 400
shots_per_randomization = 16
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 = generate_random_pauli_lindblad_noise(
box_layers, num_qubits, eplg=1e-2, seed=42
)
# a local noisy simulator in place of a QPU backend
noisy_executor = Executor(
AerSimulator(),
options={"simulator": {"layer_noise_model": layer_noise_model}},
)
task = MitigationTask()
program = task.prepare(
circuit=mirrored_circuit,
observables=observable,
parameters=None,
num_randomizations=num_randomizations,
shots_per_randomization=shots_per_randomization,
custom_boxing_options=boxing_options,
)
program._semantic_role = "simulation" # flag the program for the simulator
job_unmit_res = noisy_executor.run(program).result()
unmitigated_res = task.postprocess(job_unmit_res)
print(f"unmitigated result: {unmitigated_res.data.evs[0]}")
unmitigated result: 0.63125
Calcule limites direto e inverso para o viés
Antes de gastar qualquer tempo de QPU com o aprendizado de ruído, o SLC usa simulação clássica para limitar a contribuição de cada erro potencial ao viés do observável. Isso é feito em duas passagens complementares:
-
Propagação direta: cada erro de Pauli é propagado para frente pelo restante do circuito e seu comutador com o observável é avaliado. Um erro que anticomuta com o observável é o único tipo que pode enviesar o resultado, então essa passagem produz um limite superior para o impacto direto de cada erro.
-
Propagação inversa: cada erro é propagado para trás até o início do circuito e seu comutador com o estado inicial é avaliado. Como o circuito começa no estado todo zero, erros que comutam com ele não podem afetar o resultado, o que aperta o limite pelo outro lado.
Juntos, os dois limites formam um intervalo justo em torno da contribuição de pior caso de cada erro para o viés. No próximo passo, esses limites são combinados com as taxas de ruído aprendidas para classificar quais erros vale a pena corrigir e quais podem ser deixados de lado com segurança. A opção de boxing inject_noise_strategy="individual_modification" é necessária aqui para que cada anotação InjectNoise no circuito em caixas tenha uma identidade única à qual os limites do SLC possam ser associados.
Para problemas de maior escala, esses cálculos podem exigir recursos computacionais clássicos substanciais.
from qiskit_addon_slc.bounds import (
compute_backward_bounds,
compute_forward_bounds,
tighten_with_speed_limit,
)
from qiskit_mitigation import PEC
slc_boxing_options = {
"add_tags": "unique_box",
"inject_noise_strategy": "individual_modification",
}
slc_pec_task = PEC()
boxed_circuit, noise_model_paulis = (
slc_pec_task.generate_slc_bounds_computation_input(
mirrored_circuit, custom_boxing_options=slc_boxing_options
)
)
forward_bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable
)
tighten_forward_bounds = tighten_with_speed_limit(
forward_bounds, boxed_circuit, noise_model_paulis, observable
)
backward_bounds = compute_backward_bounds(boxed_circuit, noise_model_paulis)
Aprenda o ruído das camadas de emaranhamento
Para calcular uma estimativa final de quanto cada erro impacta o viés do valor esperado, bem como de quão caro seria corrigi-lo, aprendemos as taxas de ruído e as aplicamos aos limites de ruído previstos que calculamos antes.
O PEC (assim como o PEC+SLC) assume um 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.
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_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,
)
# first learn only the layers PEC will inject noise into, i.e. not the measurement layer
pec_layers = [
layer
for layer in slc_pec_task.find_unique_layers(
mirrored_circuit, slc_boxing_options
)
if get_annotation(layer.operation, InjectNoise)
]
layers = slc_pec_task.find_unique_layers(mirrored_circuit, slc_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(), 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()
Use o ruído aprendido para combinar os limites calculados em uma matriz de escalas locais
Com os limites clássicos (do passo anterior) e as taxas de ruído aprendidas em mãos, agora podemos calcular uma matriz de escalas locais — um peso por erro que determina com que intensidade o PEC deve cancelar cada termo de ruído individual.
A ideia central do SLC é que nem todos os erros precisam ser totalmente cancelados. Um erro cujos limites direto e inverso são ambos pequenos tem efeito desprezível sobre o observável, independentemente de sua taxa, e portanto pode ficar parcial ou totalmente sem mitigação. merge_bounds combina os limites clássicos com as taxas de Pauli-Lindblad aprendidas, e então compute_local_scales atribui a cada erro uma escala entre 0 (completamente ignorado) e 1 (totalmente cancelado), sujeita a uma bias_tolerance especificada pelo usuário. Erros com uma contribuição de viés estimada grande recebem uma escala próxima de 1 (tratamento PEC completo), enquanto erros com contribuição pequena recebem uma escala menor, reduzindo o overhead de amostragem que carregam.
O SLC expõe um controle contínuo entre os dois extremos — o PEC simples (viés zero, maior overhead) e nenhuma mitigação (overhead zero, viés de ruído total). O parâmetro bias_tolerance controla esse ajuste: defini-lo como 0 recupera o PEC completo, enquanto valores maiores permitem mais viés residual em troca de um overhead de amostragem γ² menor.
Este passo varre bias_tolerance em uma faixa de valores e calcula o overhead de amostragem resultante para cada um. Plotar esses pontos contra o overhead do PEC completo ajuda você a escolher um ponto de trabalho que caiba no seu orçamento de shots. Por exemplo, tolerar um viés residual de 5 % pode reduzir o overhead de amostragem em uma ordem de grandeza em comparação com o PEC completo, tornando viável nos QPUs de hoje um cálculo que de outra forma não seria.
import matplotlib.pyplot as plt
from qiskit_addon_slc.bounds import compute_local_scales, merge_bounds
merged_bounds = merge_bounds(
boxed_circuit,
tighten_forward_bounds,
backward_bounds,
noise_rates=learned_noise,
)
biases = []
costs = []
for bias in [0.0, *np.arange(0.01, 0.11, 0.01).tolist()]:
_, cost_, bias_ = compute_local_scales(
boxed_circuit,
merged_bounds,
noise_rates=learned_noise,
bias_tolerance=bias,
)
biases.append(bias_)
costs.append(cost_)
# Compute tha full PEC overhead and compare it with the overhead and bias of different SLC configurations
vanilla_gamma = slc_pec_task.calculate_gamma(
slc_pec_task.boxed_circuit, learned_noise, 1
)
xticks = np.arange(0, 11, 2)
fig, ax = plt.subplots()
ax.scatter(
[0], [vanilla_gamma**2], marker="D", c="tab:orange", label="full PEC"
)
ax.plot(
100 * np.array(biases),
np.array(costs),
"o-",
c="tab:blue",
label="PEC+SLC",
)
ax.set_xticks(xticks, [f"{x:.2f}" for x in xticks])
ax.set_xlabel("Remaining bias [%]")
ax.set_ylabel(r"Sampling overhead, $\gamma^2$")
ax.grid()
ax.legend()
fig.suptitle("PEC sampling overhead reduction due to SLC")
Optimal spacetime partitioning not implemented! Just partitioning list of noisy boxes.
Text(0.5, 0.98, 'PEC sampling overhead reduction due to SLC')
Escolha o limite de viés desejado com base no compromisso entre overhead de shots e viés
Neste guia escolhemos uma tolerância de viés de 5%. Observe que esse cálculo de viés se baseia no modelo de ruído aprendido, que pode não estar perfeitamente alinhado com o ruído real do dispositivo.
chosen_bias_threshold = 0.05
local_scales, sampling_cost, bias = compute_local_scales(
boxed_circuit, merged_bounds, learned_noise, bias_tolerance=bias
)
slc_pec_task.set_slc_local_scales(
local_scales, float(np.sqrt(sampling_cost)), bias
)
Prepare um programa PEC + SLC, execute-o e faça o pós-processamento
Com as escalas locais escolhidas, podemos construir o programa de execução completo PEC+SLC. A chamada PEC.prepare constrói um QuantumProgram que codifica a estratégia de injeção de anti-ruído nas anotações de caixa do circuito. Passar noise_gain="slc_local_scales" instrui o executor a usar os pesos por erro definidos por set_slc_local_scales, em vez de aplicar o mesmo ganho de ruído inverso completo a todos os erros, como faria o PEC simples.
Para produzir uma comparação justa, uma segunda tarefa PEC é preparada com noise_gain=0, o que corresponde ao cancelamento PEC completo sem margem de viés do SLC. Os dois programas são adicionados ao mesmo QuantumProgram para compartilharem um único job de execução — seus circuitos são intercalados e executados juntos, o que significa que qualquer deriva do hardware afeta ambos igualmente.
Depois que o job termina, postprocess converte as bitstrings de medição brutas em valores esperados, aplicando o fator de reescalonamento γ apropriado do PEC para cada tarefa.
program_pec = slc_pec_task.prepare(
circuit=mirrored_circuit,
observables=[observable],
parameters=None,
num_randomizations=num_randomizations,
shots_per_randomization=shots_per_randomization,
custom_boxing_options=slc_boxing_options,
noise_maps=learned_noise, # PEC option
noise_gain="slc_local_scales", # PEC option. Defines the usage of SLC
max_sampling_overhead=100, # PEC option
)
full_pec_task = PEC()
program_pec = full_pec_task.prepare(
circuit=mirrored_circuit,
observables=[observable],
parameters=None,
num_randomizations=num_randomizations,
shots_per_randomization=shots_per_randomization,
custom_boxing_options=boxing_options,
quantum_program=program_pec,
noise_maps=learned_noise, # PEC option
noise_gain=0, # PEC option
max_sampling_overhead=100, # PEC option
)
# Execute
program_pec._semantic_role = (
"simulation" # flag the program for the simulator
)
job_pec_results = noisy_executor.run(program_pec).result()
# post-processing
pec_slc_res = slc_pec_task.postprocess(job_pec_results)
full_pec_res = full_pec_task.postprocess(job_pec_results)
Compare os valores esperados mitigados e o overhead de shots
O valor esperado ideal do circuito espelhado é 1 por construção, então o viés de qualquer estimativa é simplesmente |1 - result|. Esta célula final imprime o viés e a contagem total de shots para três cenários lado a lado:
-
Sem mitigação: o valor esperado ruidoso bruto, sem nenhum cancelamento de erro aplicado.
-
PEC completo: cancelamento de erro com viés zero usando o modelo de ruído inverso completo em cada camada, com o overhead de amostragem γ² completo.
-
PEC+SLC: cancelamento de erro guiado pelas escalas locais de shaded lightcone, tolerando a
bias_toleranceescolhida em troca de uma contagem de shots substancialmente menor.
Comparar as três linhas mostra o benefício concreto do SLC: o viés mitigado do PEC+SLC fica bem abaixo do viés sem mitigação, enquanto o número total de shots é significativamente menor do que o exigido pelo PEC completo.
print(
f"Unmitigated result bias: {np.abs(1 - unmitigated_res.data.evs[0]) * 100:.2f}%, \
total shots ran: {job_unmit_res[0]['_meas'].shape[0] * job_unmit_res[0]['_meas'].shape[2]}"
)
print(
f"Full PEC final result bias: {np.abs(1 - full_pec_res.data.evs[0]) * 100:.2f}%, \
total shots ran: {job_pec_results[1]['_meas'].shape[0] * job_pec_results[1]['_meas'].shape[2]}"
)
print(
f"PEC+SLC final result bias: {np.abs(1 - pec_slc_res.data.evs[0]) * 100:.2f}%, \
total shots ran: {job_pec_results[0]['_meas'].shape[0] * job_pec_results[0]['_meas'].shape[2]}"
)
Unmitigated result bias: 36.88%, total shots ran: 6400
Full PEC final result bias: 0.97%, total shots ran: 48528
PEC+SLC final result bias: 1.46%, total shots ran: 24480