Cancelamento probabilístico de erros com lightcones sombreados
Uso estimado: 10 minutos em um processador Heron (NOTA: Esta é apenas uma estimativa. O tempo de execução real pode variar.)
Resultados de aprendizado
-
O que é o cancelamento probabilístico de erros (PEC) e por que seu overhead de amostragem cresce exponencialmente com o ruído total atuando no circuito
-
Como os lightcones sombreados (SLC) limitam a contribuição de cada termo de ruído para o observável alvo, para que você possa gastar o orçamento de mitigação onde importa — e como, além da redução de overhead com viés zero já fornecida por um lightcone puramente geométrico, o sombreamento permite trocar um viés residual limitado por um overhead de amostragem ainda menor
-
Como aprender o ruído de camada com
NoiseLearnerV3e injetar anti-ruído através dosamplomatice da primitivaExecutor -
Como combinar PEC e PEC+SLC com TREX e pós-seleção para estimar um valor esperado em hardware
Pré-requisitos
-
O workflow de padrões do Qiskit
-
Usar a primitiva Estimator para calcular valores esperados de um observável
-
Técnicas de mitigação de erros como twirling de Pauli e TREX, abordadas em Combinar opções de mitigação de erros com a primitiva Estimator
Contexto
Este tutorial demonstra como mitigar erros usando o addon shaded lightcone (SLC). Este addon é uma evolução da técnica de cancelamento probabilístico de erros (PEC), na qual um usuário aprende o ruído de camadas únicas em um circuito e depois cancela o ruído aplicando gates de um qubit e técnicas de pós-processamento. Comparado a outros métodos, PEC oferece limites mais robustos sobre o viés do resultado mitigado, mas tende a sofrer de um overhead maior em termos de tempo de QPU. Durante o PEC, para compensar a atenuação do valor esperado pelo ruído, o resultado médio é reescalado por um fator de , onde é a taxa de ruído aprendida do erro de Pauli na camada do circuito. Esse reescalonamento aumenta a variância por um fator de , e assim também multiplica o número de execuções de circuito necessárias na QPU por , o que chamamos de custo de amostragem ou overhead de amostragem. Como cresce exponencialmente, o PEC costuma ser limitado a circuitos rasos ou com poucos qubits. Saiba mais sobre PEC em Probabilistic error cancellation with sparse Pauli-Lindblad models on noisy quantum processors.
Se conseguirmos identificar erros que não precisam ser mitigados, podemos diminuir esse custo de amostragem exponencialmente. Um primeiro passo nessa direção é implementar a mitigação de erros com consciência local, que usa um "lightcone" convencional rapidamente computável para reduzir o overhead do PEC ao limitar a sensibilidade de um observável a erros ao longo do circuito, estendendo a viabilidade do PEC para escalas maiores em alguns problemas. Erros fora desse lightcone não podem afetar o resultado medido e, portanto, podem ser excluídos do procedimento de cancelamento de erros. Essa exclusão diminui o overhead de amostragem, em alguns casos substancialmente, sem introduzir viés adicional. Em particular, para medir um observável local de um circuito de profundidade fixa, o overhead de amostragem necessário eventualmente se estabiliza ao escalar o número de qubits no circuito (veja a Fig. 2b em Locality and error mitigation of quantum circuits).
Os shaded lightcones (SLC) vão além, usando simulações clássicas para limitar de forma mais rigorosa a sensibilidade a erros ao longo do circuito. Isso troca um pouco de tempo de QPU por tempo de CPU e reduz o overhead de amostragem necessário para renormalizar o viés. Em vez de um corte rígido, cada erro potencial no circuito recebe uma "sombra" graduada que limita superiormente a suscetibilidade do valor esperado do observável a esse erro. (Para os limites propagados para trás introduzidos abaixo, essa suscetibilidade também depende do estado inicial do circuito — o estado todo-zero em fluxos de trabalho típicos, e neste tutorial.) Essa caracterização refinada permite aplicações mais eficientes e direcionadas de PEC com variância reduzida, ao mesmo tempo em que dá ao usuário a capacidade de ajustar de forma controlável o viés na estimativa do observável. Veja Lightcone shading for classically accelerated quantum error mitigation para mais detalhes.
Nosso fluxo de trabalho para o addon SLC aproveita a biblioteca samplomatic junto com as classes QuantumProgram e Executor adicionadas ao Qiskit Runtime no qiskit-ibm-runtime v0.47.0, permitindo que os usuários tenham um controle mais modular das configurações de execução para supressão e mitigação de erros, mantendo a facilidade de uso. Leia mais no guia directed execution model.
Visão geral do fluxo de trabalho de mitigação de erros com SLC
Para modelar o ruído da QPU, usamos um modelo de ruído esparso de Pauli-Lindblad com taxas de erro de Pauli de um e dois qubits, geradas localmente em cada qubit e aresta do dispositivo. Com essa escolha, o fluxo de trabalho de mitigação de erros com SLC apresentado neste tutorial é o seguinte:
a. CPU — Limitar o impacto por erro de erros de Pauli de um e dois qubits
-
Propagação para frente (limitar efeito no observável). Propague cada erro até o final do circuito e calcule seu comutador com o observável.
-
Trunque termos de operador durante a evolução para manter o cálculo tratável.
-
Aperte ainda mais esses limites com uma propagação reversa aproximada do observável baseada em limites de velocidade quântica.
-
-
Propagação para trás (limitar efeito no estado inicial). Propague cada erro até o início do circuito e calcule seu comutador com o estado inicial.
b. QPU — Aprender taxas de ruído. Use NoiseLearnerV3 para estimar taxas do modelo de ruído de Pauli-Lindblad.
c. CPU — Priorizar mitigação
-
Atualizar limites combinados com taxas de ruído aprendidas. Combine os limites de propagação para frente e para trás calculados anteriormente e atualize-os com as taxas de ruído aprendidas.
-
Classificar componentes de ruído a mitigar usando os limites calculados e as taxas aprendidas. Priorize cada possível erro de ruído com base em seu impacto estimado no viés e no custo associado para corrigi-lo.
d. QPU — Inserir anti-ruído e executar. Execute o circuito de interesse com anti-ruído (ruído inverso) especificado usando anotações Box.
e. CPU — Estimar observável. Calcule o valor esperado, aplicando pós-seleção baseada em medição para reduzir o impacto de ruído não-Markoviano.
Visão geral do aprendizado de ruído
O aprendizado de ruído é uma etapa comum em vários métodos de mitigação de erros, realizada pelo noise learner; ela também aparece no tutorial de amplificação probabilística de erros. No NoiseLearnerV3, você pode identificar especificamente as camadas de ruído a serem aprendidas como objetos CircuitInstruction, de modo que você possa calcular os limites de ruído SLC desejados para cada camada da forma descrita acima. O modelo de Pauli-Lindblad aprendido fornece coeficientes a serem usados na priorização de PEC+SLC. A forma como os gates são agrupados em camadas pode ser determinada usando as funções de conveniência generate_boxing_pass_manager e find_unique_box_instructions, que são então alimentadas na função utilitária SLC generate_noise_model_paulis, conforme descrito na Etapa 2 abaixo.
| Parte 1 | Parte 2 | Parte 3 |
|---|---|---|
| Fazer twirling de Pauli em camadas de gates de dois qubits | Repetir pares de identidade de camadas e aprender ruído | Derivar uma fidelidade (erro para cada canal de ruído) |
![]() | ![]() |
Visão geral do pós-processamento
Depois de executar em hardware quântico usando o framework samplomatic e Executor, convertemos nossas medições de bitstrings no valor observável desejado. Neste tutorial, demonstramos o fluxo de trabalho em um circuito de Ising espelhado (construído na Etapa 1 abaixo), para o qual o valor esperado ideal do observável medido é 1 porque a segunda metade do circuito desfaz a primeira. Ao calcular o valor do observável com a função executor_expectation_values, aplicamos algumas técnicas de pós-processamento que reduzem o impacto do ruído. Elas incluem remover shots afetados por ruído não-Markoviano, mitigação de erro de leitura e considerar detalhes da nossa implementação de PEC. Os detalhes são discutidos na Etapa 4 abaixo.
Requisitos
Antes de iniciar este tutorial, certifique-se de ter instalado o seguinte:
-
Qiskit SDK v2.2 ou posterior, com suporte a visualização
-
Qiskit Runtime v0.47 ou posterior (
pip install qiskit-ibm-runtime) -
Shaded lightcones Qiskit addon v0.1 ou posterior (
pip install qiskit-addon-slc) -
Qiskit addon utils v0.3 ou posterior (
pip install qiskit-addon-utils) -
Samplomatic v0.13 ou posterior (
pip install samplomatic)
Configuração
Primeiro, importe os pacotes e funções necessários para executar este notebook.
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-slc qiskit-addon-utils qiskit-ibm-runtime samplomatic
from multiprocessing import set_start_method
# Setting this value prevents itertools.starmap deadlock on UNIX systems
set_start_method("spawn")
# Needed to prevent PySCF from parallelizing internally (SLC only)
%set_env OMP_NUM_THREADS=1
env: OMP_NUM_THREADS=1
import numpy as np
from matplotlib import pyplot as plt
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager, PassManager
from qiskit_ibm_runtime import (
QiskitRuntimeService,
QuantumProgram,
Executor,
NoiseLearnerV3,
)
import samplomatic
from samplomatic.utils import find_unique_box_instructions
from samplomatic.transpiler import generate_boxing_pass_manager
from qiskit_addon_utils.exp_vals.measurement_bases import (
get_measurement_bases,
)
from qiskit_addon_utils.exp_vals.expectation_values import (
executor_expectation_values,
)
from qiskit_addon_utils.noise_management import (
gamma_from_noisy_boxes,
trex_factors,
)
from qiskit_addon_utils.noise_management.post_selection import PostSelector
from qiskit_addon_utils.noise_management.post_selection.transpiler.passes import (
AddPostSelectionMeasures,
AddSpectatorMeasures,
)
from qiskit_addon_slc.bounds import (
compute_backward_bounds,
compute_forward_bounds,
compute_local_scales,
merge_bounds,
tighten_with_speed_limit,
)
from qiskit_addon_slc.utils import (
generate_noise_model_paulis,
map_modifier_ref_to_ref,
)
from qiskit_addon_slc.visualization import draw_shaded_lightcone
Exemplo de simulador em pequena escala
Como outros métodos de mitigação de erros baseados em aprendizado, o PEC com shaded lightcones mitiga o ruído físico de um processador quântico específico, então depende de serviços de hardware sem um análogo significativo em um simulador ideal:
-
O
NoiseLearnerV3caracteriza experimentalmente o canal de ruído esparso de Pauli-Lindblad em cada camada única de dois qubits. Em um simulador sem ruído não há ruído a cancelar. -
A primitiva
Executoramostra os circuitos com twirling e injeção de anti-ruído gerados pelosamplomaticem um backend.
O cálculo do limite do shaded lightcone é clássico, mas só é significativo em relação às taxas de ruído do hardware aprendidas, que definem o orçamento de mitigação e o overhead de amostragem. Por essas razões, pulamos o exemplo do simulador em pequena escala e demonstramos o fluxo de trabalho completo de PEC+SLC diretamente no hardware, com cada etapa do padrão Qiskit detalhada abaixo.
Exemplo de hardware em grande escala
Executamos o fluxo de trabalho completo de PEC+SLC em um circuito de Ising espelhado de 20 qubits executado em hardware IBM Quantum®, seguindo as quatro etapas de um padrão Qiskit.
Etapa 1: Mapear o problema
Nossa demonstração é configurada da seguinte forma:
-
Escolhemos realizar a evolução temporal sob um hamiltoniano de cadeia de Ising 1D. A cadeia de Ising 1D fornece uma estrutura de circuito bem densa, o que é conveniente para demonstrar implementações de PEC.
-
Escolhemos medir o observável , que contém um componente e um componente . Isso é conveniente para destacar como os limites do SLC tratam diferentes tipos de erros de forma diferente.
-
Queremos que o valor esperado ideal desse observável seja exatamente 1, de modo que a qualidade das estimativas mitigadas seja fácil de julgar. Espelhar o circuito nos leva à maior parte do caminho: para cada gate na segunda metade do circuito há um gate inverso na primeira metade, de modo que o circuito ideal atue como a identidade.
-
Por fim, obter o valor esperado ideal de 1 requer que o circuito comece — e, portanto, graças ao espelhamento, idealmente também termine — em um autoestado +1 do observável. Como o observável contém um componente não-, e a primitiva
Executorleva em conta a base de medição desejada no final do circuito, prependemos os gates correspondentes de preparação de base no início do circuito espelhado com uma funçãoprepare_basis. Usamos a funçãoget_measurement_basespara identificar quais gates são necessários e onde colocá-los, mantendo o controle das sutilezas de índice de qubit decorrentes das convenções de anotaçãobox, conforme discutido na seção sobre preparação de medições de base canônica.
# Width of the Ising chain (a free parameter of this demonstration)
num_qubits = 20
# The observable to measure: one X component (on qubit 6) and one Z component
# (on qubit 13), as motivated in "Step 1: Map the problem" above
target_obs_sparse = [("XZ", [6, 13], 1.0)]
observable = SparsePauliOp.from_sparse_list(
target_obs_sparse, num_qubits=num_qubits
)
# Determine the measurement basis each qubit requires for this observable.
# The reverser is used during post-processing in Step 4.
bases_virt, reverser_virt = get_measurement_bases(observable)
# Free parameters of the demonstration circuit: the number of Trotter steps
# sets the circuit depth, and rx_angle is the transverse-field rotation angle
# applied in each step (pi/4 makes the dynamics non-Clifford)
num_trotter_steps = 10
rx_angle = np.pi / 4
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
# Instantiate the mirrored circuit and prepend the basis-preparation gates
circuit = construct_ising_circuit(num_qubits, num_trotter_steps, rx_angle)
mirrored_circuit = mirror_circuit(circuit)
mirrored_circuit = prepare_basis(mirrored_circuit, bases_virt[0])
mirrored_circuit.draw(
"mpl", fold=-1, scale=0.3, idle_wires=False, measure_arrows=False
)

Etapa 2: Otimizar
Otimizamos detalhes associados ao circuito a ser executado, o observável a ser medido e os parâmetros de aprendizado de ruído. Como ponto de partida, selecionamos o processador Heron menos ocupado e o instanciamos com gates fracionários ativados. Esses gates fracionários permitem maior sensibilidade em parte de nossa filtragem de pós-seleção.
# Initialize IBM Quantum Compute Service using your saved credentials
service = QiskitRuntimeService()
# Select the least busy Heron device. This tutorial targets Heron processors:
# the usage estimate and the non-Markovian-noise post-selection discussed in
# Step 4 are specific to this processor family.
backend = service.least_busy(
operational=True,
simulator=False,
filters=lambda b: b.processor_type["family"] == "Heron",
)
# Re-fetch with fractional gates enabled (least_busy does not forward this)
# Fractional gates are enabled so the non-Clifford Rx rotations are supported natively.
backend = service.backend(backend.name, use_fractional_gates=True)
print(f"Selected backend: {backend.name}")
Selected backend: ibm_kingston
Primeiro, vamos transpilar nosso circuito para instruções ISA, conforme necessário para execução em nossas QPUs. A escolha dos qubits físicos em que executar tem um grande impacto na qualidade dos resultados, porque os tempos de coerência e as fidelidades dos gates variam ao longo de um dispositivo e mudam ao longo do tempo. Para um experimento cuidadoso, pode valer a pena selecionar manualmente uma cadeia de qubits com base em experimentos rápidos de benchmarking (veja, por exemplo, o tutorial Real-time benchmarking for qubit selection). Para os propósitos deste tutorial, confiamos no transpiler do Qiskit para selecionar um layout com base no mapa de acoplamento do backend e em seus dados de calibração mais recentes.
isa_pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
isa_circuit = isa_pm.run(mirrored_circuit)
# The chain of physical qubits selected by the transpiler
layout = isa_circuit.layout.final_index_layout()
print(f"Selected layout: {layout}")
isa_observable = observable.apply_layout(
layout, num_qubits=isa_circuit.num_qubits
)
Selected layout: [71, 58, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 36, 21, 22, 23, 16, 3, 2]
wire_order = layout + [
q for q in range(isa_circuit.num_qubits) if q not in layout
]
isa_circuit.draw(
"mpl",
fold=-1,
scale=0.3,
idle_wires=False,
wire_order=wire_order,
measure_arrows=False,
)

Colocar o circuito em boxes
Para facilitar a implementação, utilizamos o passe de transpilação generate_boxing_pass_manager, que coloca as instruções do circuito em boxes anotados. Esses boxes indicam claramente onde, no caso do PEC, o anti-ruído deve ser injetado no circuito. Para detalhes sobre configurações, consulte a documentação do Samplomatic.
Observe que o fluxo de trabalho do SLC requer o uso de inject_noise_strategy="individual_modification" mais adiante no processo, porque isso nos permite identificar de forma exclusiva as anotações InjectNoise das instruções BoxOp no circuito. (Boxes sem uma anotação InjectNoise não podem ser identificados dessa forma.)
Os cálculos de limite do SLC atualmente presumem que as anotações InjectNoise são colocadas com site="after" (veja qiskit-addon-slc#33). Como o valor padrão dessa configuração mudou em versões recentes do samplomatic (veja samplomatic#372), definimos inject_noise_site="after" explicitamente abaixo.
A função find_unique_box_instructions percorre o circuito em boxes fornecido e identifica aqueles que têm camadas ou medições únicas de dois qubits (2Q), para fins de aprendizado de ruído e injeção de ruído.
# Box circuit with Twirl and InjectNoise annotations
boxes_pm = generate_boxing_pass_manager(
twirling_strategy="active",
inject_noise_strategy="individual_modification",
inject_noise_site="after",
inject_noise_targets="gates",
measure_annotations="all",
)
boxed_circuit = boxes_pm.run(isa_circuit)
# Find the unique instructions (layers) from boxed circuit
unique_2q_instructions = find_unique_box_instructions(
boxed_circuit, normalize_annotations=None, undress_boxes=True
)
boxed_circuit.draw(
"mpl",
fold=-1,
scale=0.3,
idle_wires=False,
wire_order=wire_order,
measure_arrows=False,
)

Preparar medições de bases canônicas
Observe que devemos ter cuidado especial para manter o controle da ordenação dos qubits. Abaixo, introduzimos a noção de canonical_qubits como um meio de atualizar apropriadamente a ordenação dos qubits ao fornecê-la ao Executor, como resultado de como a ordem dos qubits é capturada ao colocar circuitos em boxes e encontrar instruções únicas. Veja a documentação Qubit ordering convention para detalhes.
# Determine the canonical qubits order
meas_box = boxed_circuit.data[-1]
canonical_qubits = [
idx
for idx, qubit in enumerate(boxed_circuit.qubits)
if qubit in meas_box.qubits
]
# map canonical qubit to physical (isa) qubit
c_2_p = {c: p for c, p in enumerate(canonical_qubits)}
# map physical (isa) qubit to virtual qubit (index in original circuit)
p_2_v = {p: v for v, p in enumerate(layout)}
# compute map between virtual and canonical qubit indices.
c_2_v = {c: p_2_v[p] for c, p in c_2_p.items()}
assert len(c_2_v) == num_qubits
bases_canon = [
np.array([base_i[c_2_v[c]] for c in range(num_qubits)], dtype=np.uint8)
for base_i in bases_virt
]
Fluxo de trabalho para shading de lightcone, aprendizado de ruído e injeção de anti-ruído
Neste tutorial, executamos os cálculos de limite do SLC antes que o aprendizado de ruído seja concluído, de modo que o Circuit a ser mitigado seja executado o mais próximo possível no tempo do modelo de ruído aprendido. Em princípio, esse workflow pode ser paralelizado ainda mais: um job de aprendizado de ruído pode ser executado enquanto, em paralelo, os limites de ruído são estimados. Para um Circuit quântico arbitrário, o cálculo do limite de ruído pode escalar com uma dependência fracamente exponencial, portanto paralelizar o cálculo do limite (por exemplo, em muitos núcleos de CPU) produz limites mais precisos para um determinado orçamento de tempo de computação, e as execuções da QPU e os cálculos de limite podem, eles próprios, ser paralelizados para o workflow mais eficiente.
Prever Paulis do modelo de ruído a serem aprendidos
Antes de executar qualquer aprendizado de ruído em hardware, primeiro enumeramos quais termos de erro de Pauli nosso modelo de ruído conterá. Lembre-se de que modelamos o ruído de cada camada única como um modelo esparso de Pauli-Lindblad com uma taxa de erro para cada Pauli de peso um (, ou ) em cada qubit ativo, e uma para cada Pauli de peso dois em cada par conectado de qubits ativos. A função generate_noise_model_paulis percorre cada camada única em box do circuito fornecido e gera exatamente essa lista de termos de Pauli, levando em conta a conectividade dos qubits do circuito.
Em outras palavras, esta etapa prevê a estrutura do modelo de ruído que o NoiseLearnerV3 preencherá posteriormente com taxas aprendidas. Conhecer essa estrutura com antecedência é o que nos permite calcular um limite para frente e um limite para trás para cada erro potencial antes mesmo de o aprendizado de ruído ter sido executado.
noise_model_paulis = generate_noise_model_paulis(
unique_2q_instructions, backend.coupling_map, boxed_circuit
)
noise_model_rates = {ref: None for ref in noise_model_paulis}
a. Calcular limites para frente
A função compute_forward_bounds avalia as relações de comutação entre os gates de cada camada e os termos de Pauli gerados acima em termos de como erros de propagação para frente afetam o observável desejado . Para gates que comutam com os termos de Pauli, nada é feito. Para gates de Clifford, eles são empurrados em direção ao início do circuito. Para gates não-Clifford, aproximamos sua influência sobre os observáveis alvo para serem priorizados posteriormente no cancelamento de ruído (depois que todos os limites tiverem sido combinados). Esse limite é obtido aplicando primeiro a norma L2 (ou seja, a raiz quadrada da soma dos quadrados dos coeficientes de termo de Pauli relevantes). Quando há termos de qubits demais envolvidos, revertemos para um limite mais frouxo que usa a desigualdade triangular.
Definir os parâmetros de cálculo de limites
As configurações a seguir controlam os cálculos clássicos de limites:
-
slc_atol: a tolerância absoluta dos cálculos. Termos de operador com coeficientes abaixo desse limite são tratados como negligenciáveis e descartados. -
slc_eigval_max_qubits: o número máximo de qubits para os quais o limite mais rigoroso baseado em autovalores (norma L2) é avaliado. Operadores suportados em mais qubits do que esse número recorrem ao limite mais frouxo da desigualdade triangular descrito acima. -
slc_evolution_max_terms: o número máximo de termos de Pauli retidos ao propagar operadores pelo circuito. Truncar os menores termos mantém o cálculo tratável ao custo de limites ligeiramente mais frouxos. -
slc_num_processes: o número de processos de CPU nos quais o cálculo de limites é paralelizado. -
slc_timeout: o orçamento de tempo (em segundos) para o cálculo de limites. Quaisquer camadas não alcançadas dentro desse orçamento recebem o limite trivial de 2.
Neste tutorial, o timeout é atingido propositalmente: mantemos slc_timeout deliberadamente pequeno para que o notebook seja executado rapidamente, e para demonstrar que atingir o timeout é seguro. Os shades calculados são limites superiores em cada etapa, então atingir o timeout apenas torna os limites mais frouxos (triviais) para as camadas que não foram alcançadas; isso nunca subestima o efeito de um erro. Isso aparece nas visualizações abaixo como as camadas mais à esquerda sendo preenchidas inteiramente com o limite trivial. Aumentar o timeout (ou o número de processos) torna os limites mais precisos e reduz ainda mais a sobrecarga de amostragem.
slc_atol = 1e-8
slc_eigval_max_qubits = 18
slc_evolution_max_terms = 1000
slc_num_processes = 8
slc_timeout = 60
forward_bounds = compute_forward_bounds(
boxed_circuit,
noise_model_paulis,
isa_observable,
evolution_max_terms=slc_evolution_max_terms,
eigval_max_qubits=slc_eigval_max_qubits,
atol=slc_atol,
num_processes=slc_num_processes,
timeout=slc_timeout,
)
Bounds computation timed out.
Visualizar o SLC para inspeção manual
Você pode interpretar o comportamento dos limites sombreados examinando como as medições e os termos de Pauli interagem com os erros locais. Esses padrões são característicos deste problema de evolução temporal do hamiltoniano de Ising kicked e também aparecem no artigo Lightcone Shading for Classically Accelerated Quantum Error Mitigation, com várias características reveladoras:
-
Podemos distinguir claramente os dois cones que surgem dos dois Paulis não-identidade no observável.
-
Podemos ver que a medição X no qubit 6 comuta com o erro X na camada mais à direita.
-
Podemos ver que o Pauli Z no qubit 13 comuta com o erro Z na camada mais à direita.
-
Quando atingimos o timeout especificado acima, as camadas restantes à esquerda são preenchidas inteiramente com limites triviais de dois.
for p in "XYZ":
display(
draw_shaded_lightcone(
boxed_circuit,
forward_bounds,
noise_model_paulis,
pauli_filter=p,
scale=0.15,
fold=-1,
idle_wires=False,
wire_order=wire_order,
measure_arrows=False,
)
)



b. Apertar limites para frente com limites de velocidade quântica
Em seguida, apertamos os limites usando a função tighten_with_speed_limit. A propagação geométrica do lightcone do observável já é totalmente considerada nos limites para frente calculados acima. O que essa função adiciona é uma passagem de autoconsistência que aproveita a velocidade limitada com que a informação pode fluir pelo circuito — o lightcone cresce no máximo um qubit por camada de gates de dois qubits — para apertar os limites previamente calculados uns em relação aos outros. A informação flui em ambas as direções:
-
Dos limites de comutador para os limites do observável propagado para trás: se o observável comuta com um erro em um qubit, então ele não pode ter muito componente ou ali.
-
Dos limites do observável propagado para trás para os limites de comutador: se o observável tem pouco componente e em um qubit, então ele deve quase comutar com ali.
forward_bounds_tighter = tighten_with_speed_limit(
forward_bounds, boxed_circuit, noise_model_paulis, isa_observable
)
Visualizar o SLC para inspeção manual
Em princípio, esse aperto proporciona uma transição mais suave entre os limites calculados e os limites triviais atribuídos às camadas além do timeout. Neste exemplo o efeito é pouco visível, porque os lightcones já se espalharam até as bordas do circuito no momento em que o timeout é atingido.
for p in "XYZ":
display(
draw_shaded_lightcone(
boxed_circuit,
forward_bounds_tighter,
noise_model_paulis,
pauli_filter=p,
scale=0.15,
fold=-1,
idle_wires=False,
wire_order=wire_order,
measure_arrows=False,
)
)



c. Calcular limites para trás
Esta parte da previsão de ruído avalia como um erro em uma camada específica pode afetar o estado de entrada . A função compute_backward_bounds primeiro inverte o circuito e remove os gates de medição, e então prossegue com uma análise semelhante à realizada para os cálculos de limites para frente.
backward_bounds = compute_backward_bounds(
boxed_circuit,
noise_model_paulis,
evolution_max_terms=slc_evolution_max_terms,
num_processes=slc_num_processes,
timeout=slc_timeout,
)
Visualizar o SLC para inspeção manual
A partir do cálculo dos limites para trás, podemos ver como a estrutura do estado inicial rege o comportamento inicial da propagação de erros:
-
Podemos ver claramente como os erros Z inicialmente comutam com o estado inicial |0⟩.
-
Apenas no qubit 6, onde inicializamos o autoestado +1 da base X, um erro Z não comuta, enquanto um erro X comuta.
for p in "XYZ":
display(
draw_shaded_lightcone(
boxed_circuit,
backward_bounds,
noise_model_paulis,
pauli_filter=p,
scale=0.15,
fold=-1,
idle_wires=False,
wire_order=wire_order,
measure_arrows=False,
)
)



Pré-visualizar limites combinados sem taxas de ruído aprendidas
A função merged_bounds determina o ponto no circuito em que a mudança dos limites para trás para os limites para frente minimiza o viés total estimado sobre o observável desejado. Esse viés é calculado como a soma das contribuições dos limites para trás para todas as localizações de ruído antes desse ponto, mais as contribuições dos limites para frente para todas as localizações de ruído após ele. Atualmente, isso é feito de forma uniforme para todos os qubits.
O ponto ideal em que mudar dos limites para trás para os limites para frente depende das taxas de ruído aprendidas. Nesta etapa ainda não aprendemos nenhuma taxa: noise_model_rates foi inicializado como None para cada camada, o que merge_bounds trata como taxas de ruído uniformes. Os limites combinados mostrados aqui são, portanto, apenas uma pré-visualização; nós os recalculamos com as taxas realmente aprendidas na Etapa 3.
merged_bounds = merge_bounds(
boxed_circuit,
forward_bounds_tighter,
backward_bounds,
noise_model_rates,
)
Missing noise rates. Partitioning backward/forward commutator bounds by assuming uniform error rates.
Optimal spacetime partitioning not implemented!Just partitioning list of noisy boxes.
Visualizar o SLC para inspeção manual
Depois de combinar os limites para trás e os limites para frente apertados, o comportamento dos SLCs combinados fica claro:
-
A função acima nos diz que uma partição é escolhida na qual ocorre a mudança dos limites para trás para os limites para frente apertados.
-
Podemos ver abaixo que os SLCs agora contêm limites parciais para trás e limites parciais para frente apertados.
for p in "XYZ":
display(
draw_shaded_lightcone(
boxed_circuit,
merged_bounds,
noise_model_paulis,
pauli_filter=p,
scale=0.15,
fold=-1,
idle_wires=False,
wire_order=wire_order,
measure_arrows=False,
)
)



Etapa 3: Executar
Nesta seção começamos a parte do fluxo de trabalho que usa um dispositivo quântico real. Para este método de mitigação de erros baseado em aprendizado, há duas etapas:
-
Aprender o ruído usando
NoiseLearnerV3. -
Executar um circuito de mitigação de erros com o framework
samplomaticeExecutor.
Com os erros limitados do nosso circuito quântico, aprendemos as taxas de ruído associadas para priorizar nosso orçamento de erro, determinar o overhead de amostragem e executar em uma QPU.
a. Aprender as taxas de ruído
O noise learner caracteriza os processos de ruído que afetam os gates em um ou mais circuitos de interesse, com base no modelo de ruído esparso de Pauli-Lindblad. O método run() inicia um job de aprendizado de ruído para as camadas únicas de dois qubits fornecidas, usando as opções especificadas na configuração do noise learner. Essas opções controlam a estratégia de twirling de Pauli, o número de randomizações e shots, as profundidades de aprendizado e a pós-seleção.
Também escolhemos as profundidades de aprendizado deliberadamente. Uma descoberta prática para mitigação baseada em aprendizado com samplomatic é que é altamente benéfico que a profundidade de aprendizado mais profunda corresponda à profundidade do circuito que você deseja mitigar. Como as layer_pair_depths do NLv3 são medidas em pares de camadas (uma camada mais sua inversa), definimos o valor mais profundo como metade da profundidade de camadas de dois qubits do circuito.
post_selection_enabled = True
# Match the deepest noise-learning depth to the depth of the circuit being
# mitigated. NLv3 ``layer_pair_depths`` are measured in layer pairs (a layer
# plus its inverse), so the deepest value is half the circuit's two-qubit-layer
# depth. Learning to this depth markedly improves the quality of the mitigation.
#
# We measure the two-qubit-layer depth on the pre-boxed ISA circuit: after
# boxing, every two-qubit gate is hidden inside a full-width ``BoxOp``, so a
# ``num_qubits == 2`` filter on ``boxed_circuit`` matches nothing (and
# ``QuantumCircuit.depth`` does not recurse into boxes).
depth_2q = isa_circuit.depth(lambda instr: instr.operation.num_qubits == 2)
max_layer_pair_depth = depth_2q // 2 # dividing by 2 since we want pairs
# Use a fixed schedule of learning depths, but drop any that exceed the circuit's
# depth and always cap the deepest value at ``max_layer_pair_depth`` so we never
# learn deeper than the circuit being mitigated.
candidate_depths = [1, 2, 4, 8, 12, 16, 24, 32, 40, 48]
layer_pair_depths = sorted(
{d for d in candidate_depths if d < max_layer_pair_depth}
| {max_layer_pair_depth}
)
noise_learner_options = {
"num_randomizations": 64,
"shots_per_randomization": 128,
"layer_pair_depths": layer_pair_depths,
"post_selection": {
"enable": post_selection_enabled,
"strategy": "edge",
"x_pulse_type": "rx",
},
"environment": {"job_tags": ["TUT_SLC"]},
}
noise_learner = NoiseLearnerV3(backend, noise_learner_options)
noise_learner_job = noise_learner.run(unique_2q_instructions)
noise_learner_result = noise_learner_job.result()
if post_selection_enabled:
print(
"Minimum fraction of shots kept for noise learning experiments: ",
end="",
)
print(
f"{min([min(d.values()) for d in [nlr.metadata['post_selection']['fraction_kept'] for nlr in noise_learner_result[:2]]]):.2f}"
)
Minimum fraction of shots kept for noise learning experiments: 0.71
# Get a dict mapping each InjectNoise.ref to its learned PauliLindbladMap
refs_2_plm = noise_learner_result.to_dict(
unique_2q_instructions, require_refs=False
)
b.i. Atualizar limites combinados com taxas de ruído realmente aprendidas
Agora que o modelo de ruído específico foi aprendido, podemos aplicar as taxas de ruído aprendidas aos limites de ruído previstos e obter uma determinação final de quais limites têm mais impacto na minimização do viés.
merged_bounds = merge_bounds(
boxed_circuit,
forward_bounds_tighter,
backward_bounds,
refs_2_plm,
)
Optimal spacetime partitioning not implemented!Just partitioning list of noisy boxes.
b.ii. Calcular os local_scales para a execução em hardware
compute_local_scales examina cada possível erro de ruído no circuito e estima o quanto esse erro poderia enviesar a medição final, bem como o quão custoso seria corrigi-lo. Em seguida, classifica os erros por quanto vale a pena mitigá-los e seleciona o subconjunto que reduz o viés o máximo possível, permanecendo dentro do orçamento de custo de amostragem permitido (ou alcançando uma precisão desejada). O resultado é um conjunto de fatores de escala indicando quais erros serão ativamente mitigados e quais permanecerão não mitigados (local_scales), junto com o overhead total previsto de custo de amostragem (sampling_costs) e o viés remanescente (residual_bias_bound).
A capacidade de controlar o viés remanescente desejado é um recurso crítico da implementação SLC do PEC. Enquanto na implementação original, o overhead de amostragem sempre visava viés zero, podemos ajustar o overhead de amostragem necessário em troca do viés remanescente esperado. Isso ajuda o usuário a permanecer dentro de um orçamento de amostragem fixo, o que pode ser particularmente útil ao prototipar inicialmente um fluxo de trabalho.
id_map = map_modifier_ref_to_ref(boxed_circuit)
summed_rates = 0.0
for box_id, noise_id in id_map.items():
learned_plm = refs_2_plm[noise_id]
summed_rates += np.sum(learned_plm.rates)
# print(f"{box_id}:\tgamma = {np.exp(2 * summed_rates):1.6e}\tsampling cost = {np.exp(4 * summed_rates):1.6e}")
total_gamma = np.exp(2 * summed_rates)
print(
f"Full PEC gamma={total_gamma}, sampling cost (gamma^2) = {total_gamma**2}"
)
Full PEC gamma=12.750876317157042, sampling cost (gamma^2) = 162.58484685543633
biases = []
costs = []
for bias in [0.0] + np.arange(0.001, 0.102, 0.01).tolist():
_, cost_, bias_ = compute_local_scales(
boxed_circuit,
merged_bounds,
refs_2_plm,
sampling_cost_budget=np.inf,
bias_tolerance=bias,
)
biases.append(bias_)
costs.append(cost_)
Balancear overhead de amostragem contra viés residual
xticks = np.arange(0, 11)
fig, ax = plt.subplots()
ax.scatter(
[0], [total_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_yscale("log")
ax.set_xticks(xticks, [f"{x:.1f}" 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")
Text(0.5, 0.98, 'PEC sampling overhead reduction due to SLC')
chosen_bias_thres = 0.1
local_scales, sampling_cost, residual_bias_bound = compute_local_scales(
boxed_circuit,
merged_bounds,
refs_2_plm,
sampling_cost_budget=np.inf,
bias_tolerance=chosen_bias_thres,
)
print(
f"PEC+SLC sampling cost (gamma^2) = {sampling_cost} "
f"w/ remaining bias = {100 * residual_bias_bound:.1f}%"
)
PEC+SLC sampling cost (gamma^2) = 48.13646509622307 w/ remaining bias = 10.0%
c. Executar o circuito de interesse com anti-ruído
c.i. Preparar o circuito modelo usando samplex
O samplex é uma saída do método build do Samplomatic, que codifica todas as informações necessárias para gerar parâmetros randomizados para template_circuit. Estes são então usados para configurar os objetos QuantumProgram, que por sua vez são executados em uma QPU com a primitiva Executor. Cada QuantumProgram pode conter vários itens, que você pode pensar como um par de template e samplex.
Veja o tutorial Hello samplomatic para detalhes.
# Build template circuit and samplex for later use with the "Executor"
template_circuit, samplex = samplomatic.build(boxed_circuit)
# Set up postselection if it's been enabled
if post_selection_enabled:
# Set up post selection PM (to add PS instructions)
post_selection_pm = PassManager(
[
AddSpectatorMeasures(backend.coupling_map),
AddPostSelectionMeasures(x_pulse_type="rx"),
]
)
final_template_circuit = post_selection_pm.run(template_circuit)
else:
final_template_circuit = template_circuit
c.ii. Configurar o QuantumProgram
num_randomizations = 4096
shots_per_randomization = 64
chunk_size = 256
# Set up QuantumProgram
program = QuantumProgram(shots=shots_per_randomization, noise_maps=refs_2_plm)
# no EM
# Collect up a dict of the other arguments that need to be bound to samplex_inputs
samplex_inputs = {
f"noise_scales.{ref}": float(0) for ref in local_scales.keys()
}
samplex_inputs |= {"basis_changes": {"basis0": bases_canon[0]}}
# Convert samplex_inputs into a dict to pass to QuantumProgram
samplex_arguments = (
samplex.inputs().bind(**samplex_inputs).make_broadcastable()
)
program.append_samplex_item(
circuit=final_template_circuit,
samplex=samplex,
samplex_arguments=samplex_arguments,
shape=(num_randomizations,),
chunk_size=chunk_size,
)
# plain PEC
# Collect a dict of the other arguments that need to be bound to samplex_inputs
samplex_inputs = {
f"noise_scales.{ref}": float(-1) for ref in local_scales.keys()
}
samplex_inputs |= {"basis_changes": {"basis0": bases_canon[0]}}
# Convert samplex_inputs into a dict to pass to QuantumProgram
samplex_arguments = (
samplex.inputs().bind(**samplex_inputs).make_broadcastable()
)
program.append_samplex_item(
circuit=final_template_circuit,
samplex=samplex,
samplex_arguments=samplex_arguments,
shape=(num_randomizations,),
chunk_size=chunk_size,
)
# PEC+SLC
# Collect a dict of the other arguments that need to be bound to samplex_inputs
samplex_inputs = {
f"noise_scales.{ref}": float(-1) for ref in local_scales.keys()
}
samplex_inputs |= {"basis_changes": {"basis0": bases_canon[0]}}
samplex_inputs |= {"local_scales": local_scales}
# Convert samplex_inputs into a dict to pass to QuantumProgram
samplex_arguments = (
samplex.inputs().bind(**samplex_inputs).make_broadcastable()
)
program.append_samplex_item(
circuit=final_template_circuit,
samplex=samplex,
samplex_arguments=samplex_arguments,
shape=(num_randomizations,),
chunk_size=chunk_size,
)
c.iii. Executar o programa com a primitiva Executor
executor = Executor(backend)
job_exec = executor.run(program)
results_exec = job_exec.result()
Etapa 4: Pós-processar
Ao calcularmos o valor esperado final de interesse usando executor_expectation_values, implementamos algumas técnicas de pós-processamento para ajudar a garantir que obtenhamos os resultados da mais alta qualidade possível. Primeiro, aplicamos nossa extinção de erro de leitura com twirling (TREX), que considera quaisquer erros ocorridos durante o processo de leitura. Em seguida, corrigimos erros devidos a ruído não-Markoviano em nossos backends Heron usando um método de pós-seleção. Esse método mede qubits ativos e espectadores, depois aplica uma rotação lenta a cada qubit e então mede novamente. Nos casos em que as duas medições não confirmam um qubit invertido como esperado, esses shots são descartados aplicando uma mask do PostSelector. Dentro do cálculo da máscara, uma estratégia específica pode ser definida para filtrar com base em nós de um único qubit ou arestas espectadoras vizinhas, o que pode influenciar tanto o número de shots filtrados quanto a qualidade dos resultados.
measurement_noise_map = noise_learner_result[2].to_pauli_lindblad_map()
trex_scale_factors = trex_factors(measurement_noise_map, reverser_virt)
post_selection_strategy = "node"
def post_process_conv(datum, steps=16, gamma=None, ps=False, trex=False):
meas = datum["meas"]
flips = datum["measurement_flips.meas"]
signs = datum.get("pauli_signs", None)
meas_basis_axis = None
avg_axis = 0
mask = None
if ps and post_selection_enabled:
# Post-select the results
post_selector = PostSelector.from_circuit(
circuit=final_template_circuit, coupling_map=backend.coupling_map
)
# Compute the ps mask for filtering results
mask = post_selector.compute_mask(
datum, strategy=post_selection_strategy
)
# Compute fraction of shots kept from post selection
total_num_shots = num_randomizations * shots_per_randomization
ps_ratio = np.sum(mask) * 100 / total_num_shots / len(bases_canon)
print(
f"With {post_selection_strategy}-based post selection ({ps_ratio:.1f}% of shots kept):"
)
results = []
for i in range(steps, num_randomizations + 1, steps):
# Compute mitigated expvals w/out post-selection
res = executor_expectation_values(
meas[:i],
reverser_virt,
meas_basis_axis,
avg_axis=avg_axis,
measurement_flips=flips[:i],
pauli_signs=signs[:i] if signs is not None else None,
postselect_mask=mask[:i] if mask is not None else None,
rescale_factors=trex_scale_factors if trex else None,
gamma_factor=gamma,
)
results.append(res[0])
return results
gamma_pec = gamma_from_noisy_boxes(refs_2_plm, id_map)
gamma_slc = gamma_from_noisy_boxes(refs_2_plm, id_map, local_scales)
steps = 16
results = {}
for label, result_idx, gamma, use_ps, use_trex in [
("PEC", 1, gamma_pec, True, True),
("PEC+SLC", 2, gamma_slc, True, True),
("Unmitigated", 0, None, False, False),
]:
res = post_process_conv(
results_exec[result_idx],
steps=steps,
gamma=gamma,
ps=use_ps,
trex=use_trex,
)
results[label] = res
With node-based post selection (24.1% of shots kept):
With node-based post selection (24.2% of shots kept):
A partir do exame dos resultados experimentais, podemos comparar diretamente o comportamento das diferentes abordagens: PEC, PEC combinado com SLC, e a linha de base não mitigada. Alguns detalhes específicos a destacar:
-
O resultado não mitigado fica bem fora da faixa de viés de 10% (próximo a 0,35) e não é afetado pelo número de randomizações.
-
Neste dispositivo, o PEC completo carrega um overhead de amostragem de . Após grandes oscilações iniciais, a estimativa de PEC puro recupera o sinal e se estabiliza em torno de 1,1, ultrapassando ligeiramente o valor exato. Observe que a faixa de viés de 10% se aplica apenas ao estimador PEC+SLC: o PEC puro é, em princípio, não enviesado, então esse desvio residual não é coberto por uma garantia de viés, mas em vez disso reflete flutuações estatísticas e imperfeições no modelo de ruído aprendido, o que é razoável nesse tamanho de problema.
-
O SLC reduz o overhead em mais ~3,4 vezes (para , para um limite de viés residual de cerca de 10%). O PEC+SLC também flutua fortemente com poucas randomizações, inicialmente chegando até a atingir a região não física, mas sua estimativa converge de forma constante para o valor exato, estabilizando-se perto de 1,0 bem dentro da faixa. Ele iguala ou supera a precisão do PEC puro a aproximadamente um terço do custo de amostragem, demonstrando o benefício do lightcone shading.
-
As barras de erro de ambas as estimativas mitigadas se contraem à medida que as randomizações se acumulam; para um número fixo de randomizações, o estimador PEC+SLC tem a menor variância estatística, consistente com seu menor .
Observe que as taxas de ruído aprendidas (e, portanto, os overheads e as estimativas mitigadas) dependem do backend e de sua calibração no momento da execução, então executar este notebook novamente pode produzir valores quantitativamente diferentes.
fig, ax = plt.subplots(1, 1, figsize=(12, 6))
ax.axhline(1.0, color="black", label="Exact")
ax.fill_between(
[-50, 4100], -10, 0, color="grey", alpha=0.25, label="Unphysical"
)
ax.fill_between([-50, 4100], 1, 10, color="grey", alpha=0.25)
ax.fill_between(
[-50, 4100], 0.9, 1.1, color="red", alpha=0.25, label="10% bias"
)
for label, res in results.items():
ax.errorbar(
list(range(steps, num_randomizations + 1, steps)),
[r[0] for r in res],
yerr=[r[1] for r in res],
alpha=0.75,
marker="o",
linestyle="",
markerfacecolor="none",
label=label,
)
ax.set_ylabel(r"$\langle X_{6}Z_{13}\rangle$")
ax.set_xlabel("# randomizations")
ax.grid()
ax.legend(ncols=2)
ax.set_ylim([-0.1, 2.0])
ax.set_xlim([-50, 4100])
(-50.0, 4100.0)

Próximos passos
Se você achou este trabalho interessante, talvez se interesse pelo seguinte material:

