Pular para o conteúdo principal
Este projeto está em fase beta. Encontrou um bug? Abra uma issue no GitHub. Tem uma pergunta ou ideia? Pergunte no Discussions — adoraríamos receber seu feedback!
Como está a qualidade da tradução nesta página?

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.

Shaded Lightcones

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:

  1. Especifique um circuito e um observável.
  2. Calcule valores esperados ruidosos, sem mitigação.
  3. Calcule limites direto e inverso para o viés.
  4. Aprenda o ruído das camadas de emaranhamento usando qiskit-noise-learning.
  5. Use o ruído aprendido para combinar os limites calculados em uma matriz de escalas locais.
  6. Escolha o limite de viés desejado com base no compromisso entre overhead de shots e viés.
  7. Prepare um programa PEC + SLC, execute-o e faça o pós-processamento.
  8. 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 θ=π4\theta= \frac{\pi}{4} 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

Output of the previous code cell

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()

Output of the previous code cell

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')

Output of the previous code cell

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_tolerance escolhida 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