Melhorar os valores esperados com a absorção de ruído propagada (PNA)
Estimativa de tempo de execução: 10 minutos em um processador Heron (NOTA: Trata-se apenas de uma estimativa. (O tempo de execução pode variar.)
Resultados do aprendizado
Ao concluir este tutorial, os usuários deverão compreender:
- O que é a absorção de ruído propagado (PNA) e como ela atenua o ruído da porta de dois qubits ao absorver canais de ruído inversos aprendidos no observável medido
- Como usar
samplomaticpara delimitar e anotar camadas de circuitos para rotação, mudanças de base e injeção de ruído - Como aprender o ruído de camada com
[NoiseLearnerV3]( /docs/guides/directed-execution-model#noiselearnerv3 ) e propagá-lo para um observável de mitigação de ruído comqiskit-addon-pna - Como amostrar circuitos aleatórios com as
QuantumProgramclasses eExecutorno Qiskit Runtime e combinar PNA com TREX e pós-seleção
Pré-requisitos
Sugerimos que os usuários estejam familiarizados com os seguintes tópicos antes de seguir com este tutorial:
- O fluxo de trabalho de padrões do Qiskit
- Utilização da primitiva Estimador para calcular os valores esperados de uma variável observável
- Técnicas de mitigação de erros, como o “Pauli twirling” e o TREX, abordadas na seção “Combinar opções de mitigação de erros com a primitiva Estimator”
Segundo plano
Neste tutorial, demonstramos como utilizar ferramentas avançadas de mitigação de erros no Qiskit para melhorar a estimativa do valor esperado em experimentos quânticos sujeitos a ruído.
O que é a absorção de ruído propagado (PNA)?
A absorção de ruído propagada é uma técnica para mitigar erros de porta, propagando o observável através do canal de ruído inverso que afeta as portas de dois qubits, resultando em um observável que atenua o ruído.
Podemos usar a mitigação de erros quânticos para extrair valores esperados úteis de hardware quântico sujeito a ruído, sem a necessidade de tolerância total a falhas. A PNA concentra-se especificamente em absorver os efeitos do ruído no próprio observável, em vez de modificar o funcionamento do circuito.
Cada porta sujeita a ruído em um circuito quântico pode ser modelada como uma porta ideal seguida por um canal de ruído. O PNA aprende ou caracteriza esses canais de ruído e define suas inversas. Em vez de inserir as operações inversas na execução do hardware (o que geralmente é inviável), o PNA propaga os canais de ruído inversos ao longo do circuito e os aplica à variável observável. Esse processo transforma o observável em um novo operador , de modo que a medição de no circuito com ruído resulta no mesmo valor esperado que a medição de em um circuito ideal, livre de ruído.
Isso pode ser resumido da seguinte forma:
- Modele cada porta com ruído como uma operação ideal seguida por um canal de ruído .
- Determine ou estime cada utilizando ferramentas de caracterização de ruído.
- Defina e propague os mapas de ruído inversos em direção à saída do circuito utilizando técnicas de transferência de Pauli.
- Incorpore esses inversos ao observável, resultando em um operador com ruído atenuado .
Quando o ruído é descrito como um canal de Pauli (ou, de forma mais geral, como um canal esparso de Pauli-Lindblad), essa propagação pode ser realizada de maneira eficiente por meio da propagação de Pauli. A propagação de Pauli oferece uma estrutura para estimar como os canais de ruído inversos se transformam à medida que passam por camadas de operações de Clifford e não-Clifford, ao mesmo tempo em que controla a complexidade computacional.
Ao transferir a mitigação para o domínio observável, o PNA evita a grande sobrecarga de amostragem que, de outra forma, resultaria da inserção de operações de correção física no circuito. Em vez disso, o circuito ruidoso original é executado, enquanto a variável observável é transformada em um novo operador , cujo valor esperado cancela os efeitos do ruído.
O fluxo de trabalho do PNA
O processo pode ser compreendido por meio das seguintes etapas conceituais. O primeiro esquema ilustra um experimento padrão com ruído.
Se aprendermos o modelo de ruído, poderemos aplicar seu inverso e cancelar o ruído.
Em vez de implementar o canal de ruído inverso por meio de amostragem na QPU, como no cancelamento probabilístico de erros (PEC), nós o aplicamos de forma clássica ao observável medido, utilizando a propagação de Pauli. O observável resultante atenua efetivamente o ruído da porta aprendido quando medido.
Mitigação modular de erros com Samplomatic e Executor
A abordagem PNA baseia-se em uma arquitetura modular de mitigação de erros no Qiskit. Essa arquitetura utiliza a samplomatic biblioteca juntamente com as QuantumProgram classes e Executor (adicionadas ao Qiskit Runtime em qiskit-ibm-runtimev0.47.0 ) para tornar técnicas como a absorção de ruído propagado e o giro de Pauli combináveis e reutilizáveis em diferentes experimentos.
Em vez de incorporar a lógica de mitigação na própria definição do circuito, a mitigação é expressa de forma declarativa por meio de samplomatic anotações e gerenciada programaticamente por meio de Executor, que controla como os circuitos aleatórios são gerados, executados e pós-processados.
Neste tutorial, implementamos um padrão do Qiskit para demonstrar como o PNA pode propagar canais de ruído de Pauli inversos e modificar o observável de acordo com isso, a fim de melhorar a estimativa dos valores esperados em QPUs sujeitas a ruído.
Visão Geral do Fluxo de Trabalho
- Etapa 1: Mapeamento para o problema quântico
- Construa um modelo de Ising com “kicked” e “Trotterizado” espelhado e uma observável-alvo.
- Etapa 2: Caracterizar e propagar o ruído
- Use
samplomaticpara identificar e anotar camadas exclusivas de dois qubits e medições no circuito. - Saiba qual ruído afeta cada camada específica usando
NoiseLearnerV3. - Mapeie cada
InjectNoiseanotação ao seu modelo de ruído aprendido correspondente. - Use
qiskit-addon-pnapara propagar os canais de ruído inversos ao longo do circuito e incorporá-los ao observável alvo.
- Use
- Etapa 3: Realizar experimentos quânticos
- Defina um
QuantumProgrampara especificar a amostragem aleatória por meio desamplexe execute os experimentos no backend usando oExecutor.
- Defina um
- Etapa 4: Reconstruir e analisar os resultados
- Compare as estratégias de mitigação (PNA, PNA+TREX, PNA+PS, PNA+PS+TREX) e visualize a melhoria em relação aos resultados sem mitigação.
Requisitos
Antes de iniciar este tutorial, certifique-se de ter os seguintes itens instalados:
- Qiskit SDK v2.2 ou versão posterior, com suporte à visualização
- Qiskit Runtime v0.47 ou posterior (
pip install qiskit-ibm-runtime) - Samplomatic v0.13 ou versão posterior (
pip install samplomatic) - Complemento PNA para o Qiskit (
pip install qiskit-addon-pna) - Utilitários do complemento Qiskit (
pip install qiskit-addon-utils)
Instalação
from qiskit import QuantumCircuit
from qiskit.quantum_info import Pauli, SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager, PassManager
from qiskit_ibm_runtime import (
QiskitRuntimeService,
QuantumProgram,
Executor,
NoiseLearnerV3,
)
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 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_pna import generate_noise_mitigating_observable
import samplomatic
from samplomatic.transpiler import generate_boxing_pass_manager
from samplomatic.annotations import InjectNoise
from samplomatic.utils import get_annotation, find_unique_box_instructions
import numpy as np
import matplotlib.pyplot as plt
# Selects a connected chain of low-error qubits on the target backend.
# The mirrored kicked-Ising circuit is a 1D chain, so we only need a line
# of connected physical qubits; the helper walks the backend's coupling map
# and grows a chain along the lowest-error two-qubit edges, so it works for
# any backend rather than relying on a hardcoded layout.
def find_qubit_chain(backend, length):
"""Find a connected chain of ``length`` physical qubits on ``backend``.
The chain is grown greedily along the lowest-error two-qubit edges, so it
favors better-performing qubits. Because the mirrored kicked-Ising circuit
is a 1D chain, a connected line is all we need.
"""
target = backend.target
# Identify the native two-qubit gate and build a per-edge error lookup.
two_qubit_gate = next(
name
for name in target.operation_names
if target[name]
and all(q is not None and len(q) == 2 for q in target[name])
)
edge_error = {
frozenset(qargs): (
1.0 if props is None or props.error is None else props.error
)
for qargs, props in target[two_qubit_gate].items()
}
graph = backend.coupling_map.graph.to_undirected(multigraph=False)
neighbors = {n: list(graph.neighbors(n)) for n in graph.node_indices()}
def first_chain_from(start):
path, visited = [start], {start}
def grow():
if len(path) == length:
return True
node = path[-1]
order = sorted(
neighbors[node],
key=lambda m: edge_error.get(frozenset((node, m)), 1.0),
)
for nxt in order:
if nxt not in visited:
visited.add(nxt)
path.append(nxt)
if grow():
return True
path.pop()
visited.remove(nxt)
return False
return path if grow() else None
def chain_cost(path):
return sum(
edge_error.get(frozenset((path[i], path[i + 1])), 1.0)
for i in range(len(path) - 1)
)
# Try low-degree qubits first (the natural ends of long chains) and keep
# the lowest-error chain found.
best_path, best_cost = None, float("inf")
for start in sorted(neighbors, key=lambda n: len(neighbors[n])):
chain = first_chain_from(start)
if chain is not None and (cost := chain_cost(chain)) < best_cost:
best_path, best_cost = chain, cost
if best_path is None:
raise ValueError(
f"Could not find a connected chain of {length} qubits "
f"on '{backend.name}'."
)
return best_pathExemplo de simulador em pequena escala
O PNA atenua o ruído físico da porta de dois qubits de um processador quântico específico. O fluxo de trabalho depende de dois serviços de hardware que não têm equivalente significativo em um simulador ideal:
NoiseLearnerV3caracteriza experimentalmente o canal de ruído esparso de Pauli-Lindblad associado a cada camada exclusiva de dois qubits do circuito transpilado. Em um simulador silencioso, não há ruído para ser aprendido, e o canal inverso propagado seria o canal de identidade.- A função
Executorprimitiveamostra os circuitos aleatórios e entrelaçados gerados porsamplomaticem um backend.
Em princípio, seria possível substituí-lo por um modelo de ruído sintético. Por exemplo, você poderia anexar PauliLindbladError instruções com o Qiskit Aer e passar o circuito com ruído resultante diretamente para generate_noise_mitigating_observable. No entanto, isso apenas valida a contabilidade clássica em relação ao ruído que você mesmo injetou e obscurece o objetivo da técnica. Por esse motivo, vamos pular o exemplo do simulador e demonstrar o fluxo de trabalho completo do PNA diretamente no hardware, com cada etapa do padrão do Qiskit detalhada a seguir.
Exemplo de hardware em grande escala
Agora executamos o fluxo de trabalho completo do PNA em um modelo de Ising com 30 nós, executado em um hardwar IBM Quantum®, seguindo as quatro etapas de um padrão do Qiskit.
Etapa 1: Mapeamento para um problema quântico
Gere o circuito de Trotter espelhado e o observável
Nesta experiência, estudaremos a dinâmica temporal de um modelo de Ising com 30 nós e efeito de “kick” em uma cadeia de spins de tipo “ 1D ”. O hamiltoniano considerado é:
,
onde descreve o acoplamento entre os spins vizinhos mais próximos, , e o campo transversal global, , é definido como . Quanto mais se afastar de um ângulo de Clifford (ou seja, ), mais difícil se torna propagar os geradores de antirruído pelo circuito.
Para a escolha da grandeza observável, consideraremos a magnetização média em um único sítio, , onde é o número de sítios.
num_qubits = 30
num_trotter_steps = 10
rx_angle = np.pi / 8
# Avg single-site magnetization
id_pauli = Pauli("I" * num_qubits)
observable = (
SparsePauliOp([id_pauli.dot(Pauli("Z"), [i]) for i in range(num_qubits)])
/ num_qubits
)
# Implement Trotterized kicked-Ising model
circuit = QuantumCircuit(num_qubits)
for _step 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)
# Append the inverse circuit to complete the mirroring
circuit.compose(circuit.inverse(), inplace=True)
circuit.measure_active()
circuit.draw("mpl", fold=-1)Output:
Etapa 2: Otimizar o problema para execução em hardware
O próximo passo é preparar nosso circuito Trotter espelhado para execução em um hardware real do IBM Quantum. A execução em uma QPU exige mais do que apenas construir o circuito abstrato, pois precisamos otimizá-lo para que:
-
Ele respeita o conjunto de portas e a conectividade nativas do backend. A transpilação mapeia o circuito lógico para um circuito ISA compatível com o backend de destino. Isso garante que toda interação entre portas e qubits seja fisicamente realizável.
-
Podemos caracterizar o ruído no nível das camadas do circuito. A PNA se baseia no aprendizado e na propagação de canais de ruído inversos. Para fazer isso de forma eficiente, dividimos o circuito transpilado em camadas “encaixotadas” exclusivas de dois qubits. Essas caixas nos permitem associar cada camada do circuito ao seu próprio modelo de ruído aprendido.
-
Podemos inserir modelos de ruído realistas no PNA. Depois que o circuito é encapsulado, utilizamos o
NoiseLearnerV3serviço para identificar experimentalmente os canais de ruído de Pauli que afetam cada camada específica de dois qubits. Esses modelos treinados são, então, vinculados de volta ao circuito por meio das anotações do Samplomatic.
Em suma, essa etapa preenche a lacuna entre um circuito espelhado idealizado e um circuito pronto para implementação em hardware com modelos de ruído aprendidos. Com essa configuração, o PNA pode propagar canais de ruído inversos pelo circuito e ajustar a variável observável de acordo com isso.
Conecte-se ao backend e compile para um circuito ISA
Primeiro, inicializamos o serviço Qiskit Runtime e selecionamos um backend. Faça a autenticação com sua própria conta seguindo as instruções para salvar suas credenciais; depois disso QiskitRuntimeService() , o sistema as recuperará automaticamente.
# Initialize the Qiskit Runtime service using your saved credentials
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=num_qubits
)
# 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}")Output:
Selected backend: ibm_fez
Em seguida, escolhemos uma cadeia conectada de qubits no backend e compilamos o circuito para ela.
Usando o find_qubit_chain auxiliar definido na seção “Configuração”, selecionamos uma linha de num_qubits qubits físicos conectados. Em seguida, fazemos a transpilagem com optimization_level=0 e definimos essa cadeia como o initial_layout, o que preserva exatamente a estrutura em camadas de portas de dois qubits do circuito espelhado. Essa estrutura é a base das etapas de agrupamento e aprendizado de ruído; portanto, um nível de otimização mais alto (que cancelaria as portas espelhadas) deve ser evitado neste caso.
# Find a connected, low-error chain of qubits on the chosen backend
layout = find_qubit_chain(backend, num_qubits)
# Transpile the circuit for the target backend, pinning the chain as the layout.
# optimization_level=0 preserves the mirrored two-qubit-gate layers that the
# boxing and noise-learning steps rely on.
pm = generate_preset_pass_manager(
backend=backend, optimization_level=0, initial_layout=layout
)
isa_circuit = pm.run(circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)
isa_circuit.draw("mpl", fold=-1)Output:
Gire as camadas de portas de dois qubits e as medições, e identifique as camadas únicas
Utilizamos samplomatic para delimitar o circuito e identificar camadas exclusivas de dois qubits. Uma caixa é uma estrutura que agrupa instruções de modo que intenções ou anotações específicas possam ser aplicadas posteriormente de maneira uniforme a todos os portões dentro da mesma caixa.
generate_boxing_pass_managerAqui, chamamos o método, que vai além da simples identificação de camadas de dois qubits. Ele desempenha várias funções essenciais:
- Agrupa todas as camadas de dois qubits no circuito,
- Aplica as
Twirlanotações eChangeBasisa essas camadas, - Agrupa as operações de medição em seções separadas por caixas, e
- Aplica a
InjectNoiseanotação a cada camada de dois qubits.
Essas anotações definem como o ruído, as mudanças de base e o twirling são tratados ao longo do circuito. Elas também estabelecem a estrutura que será utilizada posteriormente para o aprendizado e a redução do ruído.
As principais opções de configuração são:
enable_gates/enable_measures: True: Coloque entre colchetes todas as camadas de portas de dois qubits e as medições finais. As portas de qubit único são representadas com a orientação à esquerda dentro das caixas.measure_annotations: all: IncluaTwirleChangeBasisanotações na caixa de medição.twirling_strategy: active: Gire todos os qubits ativos em cada caixa que contenha portas de entrelaçamento.Twirl``inject_noise_targets: gates: AdicioneInjectNoiseanotações a todas as caixas anotadas que contenham portas de entrelaçamento.inject_noise_strategy: uniform_modification: Ajuste o nível de todas as camadas de ruído de maneira equivalente em todo o circuito.
# Box up circuit with Twirl and InjectNoise annotations
pm = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
measure_annotations="all",
twirling_strategy="active",
inject_noise_targets="gates",
inject_noise_strategy="uniform_modification",
)
boxed_circuit = pm.run(isa_circuit)draw_circ = QuantumCircuit(boxed_circuit.num_qubits)
draw_circ.append(boxed_circuit.data[0], qargs=boxed_circuit.data[0].qubits)
draw_circ.append(boxed_circuit.data[1], qargs=boxed_circuit.data[1].qubits)
draw_circ.draw("mpl", fold=-1, scale=0.3, idle_wires=False)Output:
Gere o circuito modelo e samplex, que definem como o circuito será amostrado.
Aqui, também incluímos medições de espectador e pós-seleção, necessárias para realizar a pós-seleção nas amostras geradas pelo Executor.
# Build template circuit and samplex for later use with the "Executor"
template_circuit, samplex = samplomatic.build(boxed_circuit)
# Add post-selection instructions to the template circuit
post_selection_pm = PassManager(
[
AddSpectatorMeasures(backend.coupling_map),
AddPostSelectionMeasures(x_pulse_type="rx"),
]
)
template_circuit = post_selection_pm.run(template_circuit)draw_circ = template_circuit.copy_empty_like()
draw_circ.data = template_circuit.data[:324]
draw_circ.draw("mpl", fold=-1, scale=0.3, idle_wires=False)Output:
Aprenda o som usando NoiseLearnerV3
Antes de podermos aplicar a PNA para mitigação de erros, precisamos primeiro caracterizar o ruído que atua em cada camada exclusiva de dois qubits e na camada de medição do nosso circuito. Para isso, utilizamos o NoiseLearnerV3 programa para aprender experimentalmente modelos de ruído para cada camada identificada anteriormente. O aluno realiza experimentos do tipo benchmarking que estimam o ruído do canal que afeta cada camada e retorna um objeto de resultado contendo o modelo aprendido.
Começamos identificando as camadas exclusivas do nosso circuito usando find_unique_box_instructions de samplomatic. Isso garante que aprendamos o ruído apenas uma vez por tipo distinto de camada, minimizando o número de experimentos e o custo total das tentativas. A lista de camadas resultante é passada para o modelo de aprendizado de ruído.
Existem alguns parâmetros-chave que controlam como o ruído é aprendido:
num_randomizations: Número de circuitos aleatórios utilizados por configuração de aprendizado.shots_per_randomization: Número de tentativas realizadas por circuito de aprendizado aleatório.layer_pair_depths: As profundidades do circuito (medidas em número de pares) a serem utilizadas nos experimentos de aprendizagem.post_selection: Permite a pós-seleção baseada em flanco, utilizandorxportas para aplicar pulsos pós-medição.
# Noise learning parameters
num_randomizations_nl = 64
shots_per_randomization_nl = 128
# Match the deepest noise-learning depth to the depth of the circuit being
# mitigated. ``layer_pair_depths`` are measured in layer pairs (a layer + its
# inverse), so the deepest value is half the circuits's two-qubit-layer depth.
# Learning to this depth improves the quality of the mitigation.
depth_2q = isa_circuit.depth(lambda instr: instr.operation.num_qubits == 2)
max_layer_pair_depth = depth_2q // 2
# 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}
)
# Find the unique instructions (layers) from the boxed-up circuit
unique_2q_layers_and_meas = find_unique_box_instructions(
boxed_circuit, normalize_annotations=None, undress_boxes=True
)
# Configure and run the noise learner on the unique layers.
# Options can be passed directly as a dictionary.
noise_learner_options = {
"num_randomizations": num_randomizations_nl,
"shots_per_randomization": shots_per_randomization_nl,
"layer_pair_depths": layer_pair_depths,
"post_selection": {
"enable": True,
"strategy": "edge",
"x_pulse_type": "rx",
},
"environment": {"job_tags": ["TUT_PNA"]},
}
noise_learner = NoiseLearnerV3(backend, noise_learner_options)
noise_learner_job = noise_learner.run(unique_2q_layers_and_meas)
noise_learner_result = noise_learner_job.result()Visualizar as taxas de ruído aprendidas
Após treinar os modelos de ruído, podemos analisar a distribuição das taxas de erro inferidas tanto para operações de um qubit quanto para operações de dois qubits. O código abaixo extrai as representações de Pauli-Lindblad a partir dos resultados de ruído aprendidos e coleta as taxas de ruído correspondentes.
Para cada camada treinada:
- Convertemos o modelo de ruído em uma lista esparsa de
(pstr, qubits, rate)tuplas, em quepstré a cadeia de Pauli que atua sobre os qubits dados erateé a taxa de erro associada. - Separamos as taxas em termos de um qubit (
len(pstr) == 1) e de dois qubits (len(pstr) == 2). - As listas de taxas são então classificadas, e seus valores medianos são calculados.
Representamos graficamente as distribuições das taxas de ruído de um qubit (vermelho) e de dois qubits (azul) em uma escala logarítmica, com seus valores medianos marcados por linhas verticais, para que possamos comparar as magnitudes relativas dos geradores de Pauli-Lindblad aprendidos. A ordem das taxas de um e dois qubits depende do dispositivo e das camadas específicas que estão sendo caracterizadas; nesta série de testes, os geradores de um único qubit ( weight-1 ) apresentam a maior taxa mediana.
hw_rates_1q = []
hw_rates_2q = []
for nlr in noise_learner_result[:2]:
plm_list = nlr.to_pauli_lindblad_map().to_sparse_list()
hw_rates_1q += [
rate for (pstr, qubits, rate) in plm_list if len(pstr) == 1
]
hw_rates_2q += [
rate for (pstr, qubits, rate) in plm_list if len(pstr) == 2
]
hw_rates_1q = sorted(hw_rates_1q)
hw_rates_2q = sorted(hw_rates_2q)
median_1q = hw_rates_1q[len(hw_rates_1q) // 2]
median_2q = hw_rates_2q[len(hw_rates_2q) // 2]
fig, ax = plt.subplots(1, 1, figsize=(14, 5))
ax.scatter(
(hw_rates_1q),
[(i) / (len(hw_rates_1q) - 1) for i in range(len(hw_rates_1q))],
color="red",
label="1q rates",
)
ax.set_xscale("log")
ax.set_ylim(0, 1.1)
ax.vlines(median_1q, 0, 1, color="red")
ax.text(median_1q * 1.1, 0.1, f"{median_1q:.2e}")
ax.scatter(
(hw_rates_2q),
[(i) / (len(hw_rates_2q) - 1) for i in range(len(hw_rates_2q))],
color="blue",
label="2q rates",
)
ax.set_xscale("log")
ax.set_ylim(0, 1.1)
ax.vlines(median_2q, 0, 1, color="blue")
ax.text(median_2q * 1.1, 0.2, f"{median_2q:.2e}")
ax.set_title("Learned noise rates")
ax.set_xlabel("Noise rate")
ax.set_yticks([])
plt.legend()Output:
<matplotlib.legend.Legend at 0x125336120>
Associar caixas de circuitos a ruídos aprendidos
Depois de obtermos os modelos de ruído para cada camada exclusiva de dois qubits, precisamos vinculá-los às anotações correspondentes InjectNoise dentro do circuito emoldurado.
A InjectNoise diretiva é uma samplomatic anotação que utiliza os “dressers” de qubit único para injetar ruído no circuito de maneira controlada e configurável. Isso permite a modelagem modular de ruído em diferentes camadas.
Cada InjectNoise anotação inclui:
InjectNoise.ref- um identificador único para a anotação. Isso é utilizado pelo objetosamplexpara atribuir corretamente o modelo de ruído correspondente.InjectNoise.modifier_ref(opcional) — uma referência secundária que permite escalar o modelo de ruído atribuído por um fator multiplicativo.
Nesta etapa, criamos um mapeamento de cada InjectNoise.ref para o modelo de ruído aprendido correspondente (PauliLindbladMap). Esse mapeamento garante que cada camada de porta de entrelaçamento no circuito seja associada ao modelo de ruído apropriado, de modo que os efeitos do ruído sejam aplicados com precisão durante a amostragem e nas etapas subsequentes de mitigação de ruído.
# map inject noise refs to pauli lindblad maps
refs_to_noise_models = {}
for instruction, result in zip(
unique_2q_layers_and_meas, noise_learner_result, strict=False
):
if inject_noise_annot := get_annotation(
instruction.operation, InjectNoise
):
refs_to_noise_models[inject_noise_annot.ref] = (
result.to_pauli_lindblad_map()
)Propagar o observável por meio do antirruído aprendido
Conforme discutido acima, isso é feito em duas etapas. Primeiro, propagamos um gerador de antirruído até o final do circuito. Depois disso, propagamos o observável por meio desse gerador evoluído. Esse processo é repetido para cada gerador antirruído do circuito. Nesta implementação, cada gerador em uma determinada camada é propagado até o final do circuito em paralelo. Além disso, o multiprocessamento Python é utilizado para realizar, em paralelo, tanto a propagação direta do antirruído quanto a propagação reversa do observável. Isso evita o acúmulo de geradores evoluídos na memória e também maximiza os recursos de computação.
Ao executar o PNA, é sempre necessário fornecer um circuito com ruído e uma variável observável. Se o seu circuito ruidoso for um circuito em caixa com InjectNoise anotações, você precisará fornecer o mapeamento que criamos na etapa anterior. Também é possível passar um circuito não encapsulado contendo PauliLindbladError instruções de qiskit-aer. Nesse caso, refs_to_noise_models não é necessário fornecer. Além dos dados principais, leve também em consideração o seguinte:
max_err_terms: O número de termos a serem mantidos em cada gerador antirruído à medida que a propagação para a frente ocorre. Aumentar esse valor geralmente melhora a precisão, mas não se garante que esse comportamento seja monotônico.max_obs_terms: O número de termos a serem mantidos no observável de mitigação de ruído, , à medida que ele é propagado de trás para frente através do antirruído evoluído. Valores maiores geralmente aumentam a precisão, mas não há garantia de que isso ocorra de forma monotônica.num_processes: O número de núcleos a serem dedicados ao processo. Lembre-se de que os geradores são propagados para a frente e aplicados à variável observável em paralelo.search_step: A etapa de retropropagação utiliza um método ganancioso para conjugar, de forma aproximada, dois operadores na base de Pauli. É possível acelerar esse método aumentandosearch_step. Veja opauli-propConsulte a documentação para obter mais informações.num_to_measure: Embora essa variável não seja um dado de entrada paragenerate_noise_mitigating_observable, nós a utilizamos para controlar quantos termos de realmente queremos medir. Aqui, medimos apenas os 30 principais termos, que são os termos originais do nosso conjunto de observáveis. Os termos foram agora reescalonados de forma que sua medição tenha o efeito de atenuar o ruído de porta aprendido. Embora medimos apenas 30 termos de , muitas vezes ainda é útil permitir que o número de termos aumente, pois isso aumenta a precisão dos fatores de escala dos termos principais.
# PNA parameters
num_processes = 8
max_err_terms = 10_000
max_obs_terms = 10_000
num_to_measure = num_qubits
obs_tilde_isa = generate_noise_mitigating_observable(
boxed_circuit,
isa_observable,
refs_to_noise_models,
max_err_terms=max_err_terms,
max_obs_terms=max_obs_terms,
num_processes=num_processes,
print_progress=True,
search_step=8,
)
p_2_v = {p: v for v, p in enumerate(layout)}
obs_tilde_virtual = SparsePauliOp.from_sparse_list(
[
(pstr, [p_2_v[p] for p in p_qubits], coeff)
for (pstr, p_qubits, coeff) in obs_tilde_isa.to_sparse_list()
],
num_qubits=num_qubits,
)
obs_tilde_virtual = obs_tilde_virtual[
np.argsort(np.abs(obs_tilde_virtual.coeffs))[::-1]
][:num_to_measure]Output:
Finished! 13740 / 13740 generators propagated.
obs_tilde_isa = obs_tilde_isa[np.argsort(np.abs(obs_tilde_isa.coeffs))][::-1]
plt.xscale("log")
plt.yscale("log")
plt.title(r"$\tilde{O}$ coeff magnitudes")
plt.ylabel("Magnitude")
plt.xlabel("Pauli term index")
plt.plot(np.abs(obs_tilde_isa.coeffs), ".")Output:
[<matplotlib.lines.Line2D at 0x124b0a120>]
Transforme as bases de medida para a forma canônica
Em seguida, encontramos um conjunto mínimo de bases de medição, de modo que possamos cobrir totalmente todos os termos de Pauli no observável medido ( muitos observáveis podem ser medidos simultaneamente, desde que comutem em cada qubit ). Como estamos medindo apenas os termos do nosso observável original, que é a soma de todos os Paulis individuaisZ , é necessária uma única base: a base “allZ ”.
Além de encontrar um conjunto de bases de medição de Pauli, precisamos mapear esses termos de Pauli para a forma canônica esperada pela Executor. Para obter mais informações sobre a ordenação canônica de qubits, acesse a documentação do Samplomatic.
meas_box = boxed_circuit.data[-1]
canonical_qubits = [
idx
for idx, qubit in enumerate(boxed_circuit.qubits)
if qubit in meas_box.qubits
]
c_2_p = {
c: p for c, p in enumerate(canonical_qubits)
} # canonical -> physical
p_2_v = {p: v for v, p in enumerate(layout)} # physical -> virtual
c_2_v = {c: p_2_v[p] for c, p in c_2_p.items()} # canonical -> virtual
meas_bases, bases_reverser = get_measurement_bases(obs_tilde_virtual)
meas_bases_canonical = [
np.array([base[c_2_v[c]] for c in range(num_qubits)], dtype=np.uint8)
for base in meas_bases
]Etapa 3: Realizar experimentos quânticos
Especifique como realizar a amostragem no QuantumProgram
Agora vamos configurar o QuantumProgram, que funciona como o contêiner central para todos os circuitos e configurações de amostragem que serão executados pelo Executor. Este objeto define como as instâncias de circuitos aleatórios são geradas, agrupadas e executadas para produzir os resultados de medição utilizados no PNA.
Um QuantumProgram pode conter vários itens, cada um composto por um circuito modelo e um objeto correspondente samplex que define como as randomizações são aplicadas. Essa abstração permite que o Executor gerencie todo o fluxo de trabalho como um único programa modular — desde a geração aleatória de circuitos até a coleta e agregação de imagens.
Nesta etapa, criamos um QuantumProgram que executa nosso experimento PNA utilizando o circuito modelo e samplex que construímos anteriormente. A configuração inclui os seguintes elementos:
template_circuit: O circuito que contém todas as portas lógicas necessárias para implementar todas as randomizações desejadas (desde randomizações de tipo “twirling” até parâmetros, e assim por diante).samplex: Um objeto que define uma distribuição de probabilidade sobre todas as randomizações possíveis do circuito, a partir das quais se fará a amostragem.samplex_arguments: Ligações necessárias para definir completamente osamplexbasis_changes: É aqui que especificamos um conjunto de bases de medição que abranja todos os termos de Pauli no observável medido.noise_scales.ref: Definimos a escala de cada camada de ruído para0.0a fim de evitar que qualquer ruído adicional seja injetado em nossas amostras.pauli_lindblad_maps: Obrigatório senoise_scalesforem passados. Isso apenas mapeia as camadas de ruído para o modelo de ruído associado.
shape: Uma tupla de forma para estender a forma implícita definida porsamplex_arguments. Os eixos não triviais introduzidos por essa extensão enumeram as randomizações.
# Control the # of shots during execution
shots_per_randomization_exec = 64
num_randomizations_exec = 6144
# Zero out the noise to prevent noise from being injected during execution.
# We only added InjectNoise annotations so PNA could associate the noise
# to layers in the circuit
samplex_inputs = {f"noise_scales.{ref}": 0.0 for ref in refs_to_noise_models}
samplex_inputs |= {"pauli_lindblad_maps": refs_to_noise_models}
# Specify the bases to measure. The samplex exposes one basis-change input per
# ChangeBasis-annotated box; here a single all-Z basis covers every term. We
# look up the basis-change interface name rather than hardcoding an index, since
# the name depends on the circuit's box structure.
bases_broadcastable = np.expand_dims(np.array(meas_bases_canonical), axis=1)
samplex_inputs |= {
spec.name: bases_broadcastable
for spec in samplex.inputs().get_specs(r"^basis_changes\.")
}
# Convert samplex_inputs into a dict to pass to QuantumProgram
samplex_arguments = (
samplex.inputs().make_broadcastable().bind(**samplex_inputs)
)
# Instantiate the QuantumProgram with the specified parameters
program = QuantumProgram(shots=shots_per_randomization_exec)
program.append_samplex_item(
circuit=template_circuit,
samplex=samplex,
samplex_arguments=samplex_arguments,
shape=(num_randomizations_exec,),
)Faça a amostragem do circuito usando o Executor
Agora que definimos nosso QuantumProgram, realizar o experimento é bem simples. Basta instanciar o Executor objeto, definir o backend e executar o programa.
# Execute (sample) the circuit
executor = Executor(backend)
job_exec = executor.run(program)
exec_results = job_exec.result()Etapa 4: Reconstruir e analisar os resultados
Para calcular um valor esperado com mitigação de erros, procedemos da seguinte forma:
- Calcule os fatores de escala do TREX com base no ruído identificado que afeta as medições,
- Gerar uma máscara para manter apenas as amostras selecionadas posteriormente, e
- Utilize a
executor_expectation_valuesfunção deqiskit-addon-utilspara combinar todos os dados em um valor esperado com erros mitigados.
# Computing the TREX factors
measurement_noise_map = noise_learner_result[2].to_pauli_lindblad_map()
trex_rescale_factors = trex_factors(measurement_noise_map, bases_reverser)
# Post-select the results
post_selector = PostSelector.from_circuit(
circuit=template_circuit, coupling_map=backend.coupling_map
)
# Compute the ps mask for filtering results
mask = post_selector.compute_mask(exec_results[0], strategy="edge")
# Compute expvals using post selected results
results = executor_expectation_values(
exec_results[0]["meas"],
bases_reverser,
meas_basis_axis=0,
avg_axis=1,
measurement_flips=exec_results[0]["measurement_flips.meas"],
pauli_signs=exec_results[0].get("pauli_signs", None),
postselect_mask=mask,
rescale_factors=trex_rescale_factors,
)Comparar estratégias de mitigação: PNA, PNA+TREX, PNA+PS, PNA+PS+TREX
Calculamos e visualizamos os valores esperados para várias variantes de mitigação com base nos Executor resultados.
bases_reverser_unmit = {Pauli("Z" * num_qubits): [observable]}
args = [
(bases_reverser_unmit, None, None),
(bases_reverser, None, None),
(bases_reverser, None, trex_rescale_factors),
(bases_reverser, mask, None),
(bases_reverser, mask, trex_rescale_factors),
]
evs = []
for reverser, postsel_mask, factors in args:
# Compute expvals using post selected results
res_ps = executor_expectation_values(
exec_results[0]["meas"],
reverser,
meas_basis_axis=0,
avg_axis=1,
measurement_flips=exec_results[0]["measurement_flips.meas"],
pauli_signs=exec_results[0].get("pauli_signs", None),
postselect_mask=postsel_mask,
rescale_factors=factors,
)
res_ps = np.array(res_ps)
evs.append(res_ps[:, 0][0])
experiments = ["PNA", "PNA+TREX", "PNA+PS", "PNA+PS+TREX"]
colors = ["#d9d9d9", "#b0b0b0", "#7f7f7f", "#4c4c4c"]
plt.bar(experiments, evs[1:], color=colors)
plt.axhline(y=1, color="green", linestyle="--", linewidth=2, label="Ideal")
plt.axhline(
y=evs[0], color="red", linestyle="--", linewidth=2, label="Unmitigated"
)
plt.ylabel("Expectation value", fontsize=14)
plt.title(
r"30q Mirrored Ising, 10 Trotter steps, $\theta_{rx}=\frac{\pi}{8}$",
fontsize=14,
)
plt.legend(loc="upper left", bbox_to_anchor=(1.05, 1), borderaxespad=0.0)
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()Output:
Os resultados demonstram os benefícios cumulativos da combinação de diferentes técnicas de mitigação de erros. A abordagem PNA simples já restaura o valor esperado para um nível próximo ao do benchmark ideal, indicando que a propagação de canais de ruído inversos na variável observável compensa efetivamente os erros das portas de dois qubits.
- A inclusão da reponderação TREX (PNA+TREX) melhora ligeiramente a estimativa, corrigindo o desequilíbrio de amostragem nos circuitos aleatórios.
- A pós-seleção (PNA+PS) proporciona um aumento mais perceptível ao filtrar resultados de medição inconsistentes que provavelmente decorrem de erros residuais.
- Por fim, a combinação das três (PNA+PS+TREX) produz o resultado mais preciso, aproximando-se bastante do valor ideal, o que demonstra como essas estratégias de mitigação se reforçam mutuamente.
De modo geral, a comparação destaca que o PNA serve como uma base robusta para a estimativa do valor esperado com consideração ao ruído, enquanto o TREX e a pós-seleção oferecem refinamentos complementares para ganhos adicionais de precisão.
Próximas etapas
Se você achou este trabalho interessante, talvez se interesse pelo material a seguir: