Combinar opções de mitigação de erro com o primitivo Estimator
Estimativa de tempo de execução: 7 minutos em um processador Heron r2 (NOTA: Trata-se apenas de uma estimativa. (O tempo de execução pode variar.)
Resultados do aprendizado
Recomendamos que os usuários estejam familiarizados com os seguintes tópicos antes de seguir com este tutorial:
- Os conceitos básicos de desacoplamento dinâmico, mitigação de erros de medição, rotação de portas e extrapolação sem ruído, conforme descrito neste guia.
Pré-requisitos
Após concluir este tutorial, os usuários deverão compreender:
- Como as técnicas de mitigação de erros mencionadas acima são implementadas seletivamente no hardware.
- Como se comparam em termos de capacidade de atenuar o ruído do hardware.
Segundo plano
Este tutorial explora as opções de supressão e mitigação de erros disponíveis na primitiva Estimator do Qiskit Runtime. Este tutorial mostra como implementar cada um dos métodos a seguir individualmente:
- Desacoplamento dinâmico
- Mitigação de erros de medição
- Giro do portão
- Extrapolação de ruído zero (ZNE)
Observe que uma alternativa à implementação dessas técnicas individualmente é implementá-las utilizando um nível de resiliência, em que resilience_level assume os valores 0, 1, 2:
- 0: Nenhuma medida de mitigação foi implementada.
- 1: Foi implementada a mitigação de erros de medição.
- 2: Foram implementadas a rotação de portas, a mitigação de erros de medição e o ZNE.
Neste tutorial, você criará um circuito e um observável e enviará tarefas utilizando a primitiva Estimator com diferentes combinações de configurações de mitigação de erros. Em seguida, você representará graficamente os resultados para observar os efeitos das diversas configurações. A maior parte do tutorial utiliza um circuito de 10 qubits para facilitar a visualização e, no final, você ampliará o fluxo de trabalho para 50 qubits.
Requisitos
Antes de iniciar este passo a passo, 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)
Instalação
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 EstimatorExemplo de simulador em pequena escala
Vamos pular esta etapa, pois a mitigação de erros de tempo de execução não é compatível com simuladores.
Exemplo de hardware
Passo 1: Mapear entradas clássicas para um problema quântico
Este passo a passo pressupõe que o problema clássico já foi mapeado para o quântico. Comece construindo um circuito e um observável para medir. Embora as técnicas utilizadas aqui se apliquem a muitos tipos diferentes de circuitos, para simplificar, este passo a passo utiliza o efficient_su2 circuito incluído na biblioteca de circuitos Qiskit.
efficient_su2 é um circuito quântico parametrizado projetado para ser executado com eficiência em hardware quântico com conectividade limitada de qubits, mantendo-se, ao mesmo tempo, suficientemente expressivo para resolver problemas em áreas de aplicação como otimização e química. É construído alternando camadas de portas de um qubit parametrizadas com uma camada que contém um padrão fixo de portas de dois qubits, por um número determinado de repetições. O padrão das portas de dois qubits pode ser definido pelo usuário. Aqui você pode usar o padrão pairwise integrado, pois ele minimiza a profundidade do circuito ao agrupar as portas de dois qubits da forma mais compacta possível. Esse padrão pode ser executado utilizando apenas conectividade linear entre qubits.
n_qubits = 10
reps = 1
circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)
circuit.decompose().draw("mpl", scale=0.7)Output:
Como nossa observável, vamos considerar o operador de Pauli atuando sobre o último qubit, . Observe que o fato de o último qubit corresponder ao primeiro elemento dessa sequência se deve ao uso da 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
)Nesta altura, você pode prosseguir com a execução do circuito e medir a grandeza observável. No entanto, você também deseja comparar o resultado do dispositivo quântico com a resposta correta — ou seja, o valor teórico da observável, caso o circuito tivesse sido executado sem erros. No caso de circuitos quânticos pequenos, é possível calcular esse valor simulando o circuito em um computador clássico, mas isso não é possível para circuitos maiores, em escala comercial. É possível contornar esse 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 pela inversão de cada porta do circuito na ordem inversa. O circuito resultante implementa o operador de identidade, que pode ser trivialmente simulado. Como a estrutura do circuito original é preservada no circuito espelho, a execução do circuito espelho ainda dá uma ideia de como o dispositivo quântico funcionaria no circuito original.
A célula de código a seguir atribui parâmetros aleatórios ao seu circuito e, em seguida, constrói o circuito espelho usando a unitary_overlap classe. Antes de espelhar o circuito, acrescente uma instrução de barreira a ele para impedir que o transpilador una as duas partes do circuito de cada lado da barreira, resultando em um circuito transpilado sem nenhuma porta lógica.
# 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)Output:
Etapa 2: Otimizar o problema para execução em hardware quântico
Você deve otimizar seu circuito antes de executá-lo no hardware. Esse processo envolve algumas etapas:
- Escolha um layout de qubit que mapeie os qubits virtuais de seu circuito para qubits físicos no hardware.
- Insira portas de troca conforme necessário para rotear interações entre qubits que não estão conectados.
- Traduza as portas em seu circuito para instruções de arquitetura de conjunto de instruções (ISA) que possam ser executadas diretamente no hardware.
- Realizar otimizações de circuito para minimizar a profundidade do circuito e a contagem de portas.
O transpiler integrado ao Qiskit pode realizar todas essas etapas para você. Como este exemplo utiliza um circuito eficiente em termos de hardware, o transpiler deve ser capaz de escolher um layout de qubits que não exija a inserção de portas de troca para o encaminhamento das 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)Output:
<IBMBackend('ibm_fez')>
Você pode compilar seu circuito para o backend de sua escolha criando um gerenciador de etapas e, em seguida, executando-o no circuito. Uma maneira fácil de criar um gerenciador de senhas é usar a generate_preset_pass_manager função. Consulte “Transpilação com gerenciadores de passagens” para obter uma explicação mais detalhada sobre a transpilação com gerenciadores de passagens.
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)Output:
O circuito transpilado agora contém apenas instruções ISA. Todas as portas foram decompostas em termos de portas de tipo “ ”, rotações do tipo “ ” e portas CZ.
O processo de transpilação mapeou os qubits virtuais do circuito para qubits físicos no hardware. As informações sobre o layout do qubit são armazenadas no atributo layout do circuito transpilado. O observável também foi definido em termos dos qubits virtuais, portanto, você precisa aplicar esse layout ao observável, o que pode ser feito 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)Output:
Original observable:
SparsePauliOp(['ZIIIIIIIII'],
coeffs=[1.+0.j])
Observable with layout applied:
SparsePauliOp(['IIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])
Passo 3: Execute usando Qiskit primitives
Agora você está pronto para executar seu circuito usando o Estimator primitivo.
Aqui você enviará cinco trabalhos separados, começando sem supressão ou atenuação de erros e ativando sucessivamente várias opções de supressão e atenuação de erros disponíveis em Qiskit Runtime. Para obter informações sobre as opções, consulte as páginas a seguir:
- Visão geral de todas as opções
- Desacoplamento dinâmico
- Resiliência, incluindo mitigação de erros de medição e extrapolação de ruído zero (ZNE)
- Girando
Como esses trabalhos podem ser executados independentemente uns dos outros, você pode usar o modo em lote para permitir que o Qiskit Runtime otimize o tempo de execução.
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)Etapa 4: Pós-processamento e retorno do resultado no formato clássico desejado
Por fim, você pode analisar os dados. Aqui você recuperará os resultados do trabalho, extrairá deles os valores de expectativa medidos 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()Output:
Nessa escala pequena, é difícil ver o efeito da maioria das técnicas de atenuação de erros, mas a extrapolação de ruído zero proporciona um aprimoramento perceptível. No entanto, observe que essa melhoria não é gratuita, pois o resultado do ZNE também tem uma barra de erro maior.
Exemplo de 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, é possível ampliá-lo para 50 qubits. A seguinte célula de código repete todas as etapas deste passo a passo, 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()Output:
Ao comparar os resultados de 50 qubits com os resultados de 10 qubits anteriores, você pode observar o seguinte (seus resultados podem ser diferentes entre as execuções):
- Todas as experiências apresentam resultados mais próximos do valor ideal e todas as barras de erro são menores.
- A inclusão do desacoplamento dinâmico pode ter prejudicado o desempenho em comparação com o caso sem medidas de mitigação. Isso não é surpreendente, pois o circuito é muito denso. O desacoplamento dinâmico é útil principalmente quando há grandes intervalos no circuito, durante os quais os qubits permanecem inativos, sem que lhes sejam aplicadas portas. Quando essas lacunas não existem, o desacoplamento dinâmico não é eficaz e pode, na verdade, prejudicar o desempenho devido a erros nos próprios pulsos de desacoplamento dinâmico. O circuito de 10 qubits talvez tenha sido pequeno demais para que pudéssemos observar esse efeito.
- Com a extrapolação sem ruído, o resultado fica muito próximo do valor ideal. Isso demonstra o poder do ZNE.
Próximas etapas
Se você achou este trabalho interessante, talvez se interesse pelo material a seguir sobre algumas técnicas adicionais de mitigação e supressão de erros que não foram mencionadas neste tutorial: