Skip to main content
IBM Quantum Platform

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 Estimator

Exemplo 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:

Output of the previous code cell

Como nossa observável, vamos considerar o operador de Pauli ZZ atuando sobre o último qubit, ZIIZ I \cdots I. 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:

Output of the previous code cell

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:

Output of the previous code cell

O circuito transpilado agora contém apenas instruções ISA. Todas as portas foram decompostas em termos de portas de tipo “ X\sqrt{X} ”, rotações do tipo “ RzR_z ” 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:

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:

Output of the previous code cell

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:

Output of the previous code cell

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

Recomendações

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:

Esta página foi útil?
Relate um bug, erro de digitação ou solicite conteúdo no GitHub.