Combine opções de mitigação de erros com a primitiva Estimator
Estimativa de uso: 7 minutos em um processador Heron r2 (NOTA: Esta é apenas uma estimativa. Seu tempo de execução pode variar.)
Resultados de aprendizado
Sugerimos que os usuários estejam familiarizados com os seguintes tópicos antes de realizar este tutorial:
- Os conceitos básicos de desacoplamento dinâmico, mitigação de erros de medição, gate twirling e extrapolação de ruído zero, conforme descrito neste guia.
Pré-requisitos
Após concluir este tutorial, os usuários devem entender:
- Como as técnicas de mitigação de erros mencionadas são implementadas seletivamente em hardware.
- Como elas se comparam em termos de capacidade de mitigar o ruído do hardware.
Contexto
Este tutorial explora as opções de supressão e mitigação de erros disponíveis com a primitiva Estimator do Qiskit Runtime. Este tutorial mostra como implementar cada um dos seguintes métodos individualmente:
- Desacoplamento dinâmico
- Mitigação de erros de medição
- Gate twirling
- Extrapolação de ruído zero (ZNE)
Observe que uma alternativa para implementar essas técnicas individualmente é implementá-las usando um nível de resiliência, em que resilience_level assume os valores 0, 1, 2:
- 0 : Nenhuma mitigação é implementada.
- 1 : A mitigação de erros de medição é implementada.
- 2 : Gate twirling, mitigação de erros de medição e ZNE são implementados.
Neste tutorial, você construirá um circuito e um observável e enviará jobs usando a primitiva Estimator com diferentes combinações de configurações de mitigação de erros. Em seguida, você plotará os resultados para observar os efeitos das várias configurações. A maior parte do tutorial usa um circuito de 10 qubits para facilitar as visualizações e, no final, você escalará o fluxo de trabalho para 50 qubits.
Requisitos
Antes de iniciar este guia, certifique-se de que você tem o seguinte instalado:
- Qiskit SDK v2.1 ou posterior, com suporte para visualização
- Qiskit Runtime v0.40 ou posterior (
pip install qiskit-ibm-runtime)
Configuração
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-runtime
import matplotlib.pyplot as plt
import numpy as np
from qiskit.circuit.library import efficient_su2, unitary_overlap
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Batch, EstimatorV2 as Estimator
Exemplo com simulador em pequena escala
Vamos ignorar esta etapa, pois a mitigação de erros em tempo de execução não é suportada em simuladores.
Exemplo em hardware
Passo 1: Mapear entradas clássicas para um problema quântico
Este guia assume que o problema clássico já foi mapeado para quântico. Comece construindo um circuito e um observável para medir. Embora as técnicas usadas aqui se apliquem a muitos tipos diferentes de circuitos, para simplicidade este guia usa o circuito efficient_su2 incluído na biblioteca de circuitos do Qiskit.
efficient_su2 é um circuito quântico parametrizado projetado para ser eficientemente executável em hardware quântico com conectividade limitada de qubits, enquanto ainda é expressivo o suficiente para resolver problemas em domínios de aplicação como otimização e química. Ele é construído alternando camadas de portas de qubit único parametrizadas com uma camada contendo um padrão fixo de portas de dois qubits, por um número escolhido de repetições. O padrão de portas de dois qubits pode ser especificado pelo usuário. Aqui você pode usar o padrão pairwise incorporado porque ele minimiza a profundidade do circuito ao empacotar as portas de dois qubits o mais densamente possível. Este padrão pode ser executado usando apenas conectividade linear de qubits.
n_qubits = 10
reps = 1
circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)
circuit.decompose().draw("mpl", scale=0.7)

Como nosso observável, vamos considerar o operador Pauli agindo no último qubit, . Observe que o fato de o último qubit corresponder ao primeiro elemento desta string deve-se ao uso de notação little-endian pelo Qiskit.
# Z on the last qubit (index -1) with coefficient 1.0
observable = SparsePauliOp.from_sparse_list(
[("Z", [-1], 1.0)], num_qubits=n_qubits
)
Neste ponto, você poderia prosseguir para executar seu circuito e medir o observável. No entanto, você também quer comparar a saída do dispositivo quântico com a resposta correta — isto é, o valor teórico do observável, se o circuito tivesse sido executado sem erro. Para circuitos quânticos pequenos você pode calcular este valor simulando o circuito em um computador clássico, mas isso não é possível para circuitos maiores em escala de utilidade. Você pode contornar este problema com a técnica do "circuito espelho" (também conhecida como "compute-uncompute"), que é útil para avaliar o desempenho de dispositivos quânticos.
Circuito espelho
Na técnica do circuito espelho, você concatena o circuito com seu circuito inverso, que é formado invertendo cada porta do circuito em ordem reversa. O circuito resultante implementa o operador identidade, que pode ser trivialmente simulado. Como a estrutura do circuito original é preservada no circuito espelho, executar o circuito espelho ainda dá uma ideia de como o dispositivo quântico se comportaria no circuito original.
A célula de código a seguir atribui parâmetros aleatórios ao seu circuito e então constrói o circuito espelho usando a classe unitary_overlap. Antes de espelhar o circuito, adicione uma instrução de barreira a ele para evitar que o transpilador mescle as duas partes do circuito em ambos os lados da barreira e resulte em um circuito transpilado sem nenhuma porta.
# Generate random parameters
rng = np.random.default_rng(1234)
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)
# Assign the parameters to the circuit
assigned_circuit = circuit.assign_parameters(params)
# Add a barrier to prevent circuit optimization of mirrored operators
assigned_circuit.barrier()
# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)
mirror_circuit.decompose().draw("mpl", scale=0.7)

Passo 2: Otimizar problema para execução em hardware quântico
Você deve otimizar seu circuito antes de executá-lo em hardware. Este processo envolve algumas etapas:
- Escolher um layout de qubits que mapeie os qubits virtuais do seu circuito para qubits físicos no hardware.
- Inserir portas swap conforme necessário para rotear interações entre qubits que não estão conectados.
- Traduzir as portas no seu circuito para instruções Instruction Set Architecture (ISA) que podem ser diretamente executadas no hardware.
- Realizar otimizações de circuito para minimizar a profundidade do circuito e a contagem de portas.
O transpilador integrado ao Qiskit pode realizar todas essas etapas para você. Como este exemplo usa um circuito eficiente para hardware, o transpilador deve ser capaz de escolher um layout de qubits que não requer a inserção de portas swap para rotear interações.
Você precisa escolher o dispositivo de hardware a ser usado antes de otimizar seu circuito. A célula de código a seguir solicita o dispositivo menos ocupado com pelo menos 127 qubits.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
print(backend)
<IBMBackend('ibm_fez')>
Você pode transpilar seu circuito para o backend escolhido criando um pass manager e então executando o pass manager no circuito. Uma maneira fácil de criar um pass manager é usar a função generate_preset_pass_manager. Consulte Transpile with pass managers para uma explicação mais detalhada sobre transpilação com pass managers.
pass_manager = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=1234
)
isa_circuit = pass_manager.run(mirror_circuit)
isa_circuit.draw("mpl", idle_wires=False, scale=0.7, fold=-1)

O circuito transpilado agora contém apenas instruções ISA. Todas as portas foram decompostas em termos de portas e rotações , e portas CZ.
O processo de transpilação mapeou os qubits virtuais do circuito para qubits físicos no hardware. A informação sobre o layout de qubits é armazenada no atributo layout do circuito transpilado. O observável também foi definido em termos dos qubits virtuais, então você precisa aplicar este layout ao observável, o que você pode fazer com o método apply_layout de SparsePauliOp.
isa_observable = observable.apply_layout(isa_circuit.layout)
print("Original observable:")
print(observable)
print()
print("Observable with layout applied:")
print(isa_observable)
Original observable:
SparsePauliOp(['ZIIIIIIIII'],
coeffs=[1.+0.j])
Observable with layout applied:
SparsePauliOp(['IIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])
Passo 3: Executar usando primitivas Qiskit
Agora você está pronto para executar seu circuito usando a primitiva Estimator.
Aqui você enviará cinco jobs separados, começando sem supressão ou mitigação de erros, e sucessivamente habilitando várias opções de supressão e mitigação de erros disponíveis no Qiskit Runtime. Para informações sobre as opções, consulte as seguintes páginas:
- Overview of all options
- Dynamical decoupling
- Resilience, including measurement error mitigation and zero-noise extrapolation (ZNE)
- Twirling
Como esses jobs podem ser executados independentemente uns dos outros, você pode usar o modo batch para permitir que o Qiskit Runtime otimize o tempo de execução deles.
pub = (isa_circuit, isa_observable)
jobs = []
with Batch(backend=backend) as batch:
estimator = Estimator(mode=batch)
estimator.options.environment.job_tags = [
"TUT_CEM_SS"
] # add tag for this small scale job
# Set number of shots
estimator.options.default_shots = 100_000
# Disable runtime compilation and error mitigation
estimator.options.resilience_level = 0
# Run job with no error mitigation
job0 = estimator.run([pub])
jobs.append(job0)
# Add dynamical decoupling (DD)
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XpXm"
job1 = estimator.run([pub])
jobs.append(job1)
# Add readout error mitigation (DD + TREX)
estimator.options.resilience.measure_mitigation = True
job2 = estimator.run([pub])
jobs.append(job2)
# Add gate twirling (DD + TREX + Gate Twirling)
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"
job3 = estimator.run([pub])
jobs.append(job3)
# Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 3, 5)
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
job4 = estimator.run([pub])
jobs.append(job4)
Passo 4: Pós-processar e retornar resultado no formato clássico desejado
Finalmente, você pode analisar os dados. Aqui você recuperará os resultados dos jobs, extrairá os valores de expectativa medidos deles e plotará os valores, incluindo barras de erro de um desvio padrão.
# Retrieve the job results
results = [job.result() for job in jobs]
# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]
# Unpack the expectation values and standard errors
expectation_vals = np.array(
[float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
[float(pub_result.data.stds) for pub_result in pub_results]
)
# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
range(len(labels)),
expectation_vals,
yerr=standard_errors,
label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")
plt.show()
Nesta pequena escala, é difícil ver o efeito da maioria das técnicas de mitigação de erros, mas a extrapolação de ruído zero dá uma melhoria perceptível. No entanto, note que essa melhoria não vem de graça, porque o resultado ZNE também tem uma barra de erro maior.
Exemplo em hardware em grande escala
Ao desenvolver um experimento, é útil começar com um circuito pequeno para facilitar as visualizações e simulações. Agora que você desenvolveu e testou nosso fluxo de trabalho em um circuito de 10 qubits, você pode escalá-lo para 50 qubits. A célula de código a seguir repete todas as etapas deste guia, mas agora as aplica a um circuito de 50 qubits.
n_qubits = 50
reps = 1
# Construct circuit and observable
circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)
observable = SparsePauliOp.from_sparse_list(
[("Z", [-1], 1.0)], num_qubits=n_qubits
)
# Assign parameters to circuit
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)
assigned_circuit = circuit.assign_parameters(params)
assigned_circuit.barrier()
# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)
# Transpile circuit and observable
isa_circuit = pass_manager.run(mirror_circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)
# Run jobs
pub = (isa_circuit, isa_observable)
jobs = []
with Batch(backend=backend) as batch:
estimator = Estimator(mode=batch)
estimator.options.environment.job_tags = [
"TUT_CEM_LS"
] # add tag for this large scale job
# Set number of shots
estimator.options.default_shots = 100_000
# Disable runtime compilation and error mitigation
estimator.options.resilience_level = 0
# Run job with no error mitigation
job0 = estimator.run([pub])
jobs.append(job0)
# Add dynamical decoupling (DD)
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XpXm"
job1 = estimator.run([pub])
jobs.append(job1)
# Add readout error mitigation (DD + TREX)
estimator.options.resilience.measure_mitigation = True
job2 = estimator.run([pub])
jobs.append(job2)
# Add gate twirling (DD + TREX + Gate Twirling)
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"
job3 = estimator.run([pub])
jobs.append(job3)
# Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 3, 5)
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
job4 = estimator.run([pub])
jobs.append(job4)
# Retrieve the job results
results = [job.result() for job in jobs]
# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]
# Unpack the expectation values and standard errors
expectation_vals = np.array(
[float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
[float(pub_result.data.stds) for pub_result in pub_results]
)
# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
range(len(labels)),
expectation_vals,
yerr=standard_errors,
label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")
plt.show()
Quando você compara os resultados de 50 qubits com os resultados de 10 qubits anteriores, você pode notar o seguinte (seus resultados podem diferir entre execuções):
- Todos os experimentos apresentam resultados mais próximos do valor ideal e todas as barras de erro são menores.
- A adição de desacoplamento dinâmico pode ter piorado o desempenho em comparação com o caso sem mitigação. Isso não é surpreendente, porque o circuito é muito denso. O desacoplamento dinâmico é útil principalmente quando há grandes lacunas no circuito, durante as quais os qubits ficam ociosos sem portas sendo aplicadas a eles. Quando essas lacunas não estão presentes, o desacoplamento dinâmico não é eficaz e pode realmente piorar o desempenho devido a erros nos próprios pulsos de desacoplamento dinâmico. O circuito de 10 qubits pode ter sido muito pequeno para observarmos esse efeito.
- Com a extrapolação de ruído zero, o resultado fica muito próximo do valor ideal. Isso demonstra o poder do ZNE.
Próximos passos
Se você achou este trabalho interessante, talvez se interesse pelo seguinte material sobre algumas técnicas adicionais de mitigação e supressão de erros que não foram mencionadas neste tutorial: