Pular para o conteúdo principal

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)

Output of the previous code cell

Output of the previous code cell

Para nosso observável, vamos considerar o operador Pauli ZZ agindo no último qubit, ZIIZ I \cdots I.

# 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)

Output of the previous code cell

Output of the previous code cell

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)

Output of the previous code cell

Output of the previous code cell

O circuito transpilado agora contém apenas instruções ISA. As portas de qubit único foram decompostas em termos de portas X\sqrt{X} e rotações RzR_z, 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:

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()

Output of the previous code cell

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()

Output of the previous code cell

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!