Combine opções de mitigação de erros com a primitiva Estimator
Estimativa de uso: Sete minutos em um processador Heron r2 (NOTA: Esta é apenas uma estimativa. Seu tempo de execução pode variar.)
Contexto
Este guia explora as opções de supressão e mitigação de erros disponíveis com a primitiva Estimator do Qiskit Runtime. 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 maioria dos exemplos usa um circuito de 10 qubits para facilitar as visualizações e, no final, você pode escalar o fluxo de trabalho para 50 qubits.
Estas são as opções de supressão e mitigação de erros que você usará:
- Desacoplamento dinâmico
- Mitigação de erros de medição
- Gate twirling
- Extrapolação de ruído zero (ZNE)
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
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)


Para nosso observável, vamos considerar o operador Pauli agindo no último qubit, .
# 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. Sem a barreira, o transpilador mesclaria o circuito original com seu inverso, resultando 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
)
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. As portas de qubit único foram decompostas em termos de portas e rotações , e as portas CX foram decompostas em portas ECR e rotações de qubit único.
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(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
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)
# 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.
Escalar o experimento
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)
# 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):
- Os resultados sem mitigação de erros são piores. Executar o circuito maior envolve executar mais portas, então há mais oportunidades para erros se acumularem.
- A adição de desacoplamento dinâmico pode ter piorado o desempenho. 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 é tão bom, ou quase tão bom, quanto o resultado de 10 qubits, embora a barra de erro seja muito maior. Isso demonstra o poder da técnica ZNE!
Conclusão
Neste guia, você investigou diferentes opções de mitigação de erros disponíveis para a primitiva Estimator do Qiskit Runtime. Você desenvolveu um fluxo de trabalho usando um circuito de 10 qubits e então o escalou para 50 qubits. Você pode ter observado que habilitar mais opções de supressão e mitigação de erros nem sempre melhora o desempenho (especificamente, habilitar o desacoplamento dinâmico neste caso). A maioria das opções aceita configuração adicional, que você pode testar em seu próprio trabalho!