Pular para o conteúdo principal
Este projeto está em fase beta. Encontrou um bug? Abra uma issue no GitHub. Tem uma pergunta ou ideia? Pergunte no Discussions — adoraríamos receber seu feedback!
Como está a qualidade da tradução nesta página?

Extrapolação para ruído zero com gate folding e extrapolação personalizada

A extrapolação para ruído zero (ZNE) é uma técnica para mitigar o ruído quântico em cálculos de valores esperados de observáveis. Ela funciona executando um circuito quântico em escalas de ruído crescentes, obtendo valores esperados para essas escalas de ruído e extrapolando para estimar um valor esperado sem ruído. Tanto a etapa de amplificação do ruído quanto a de extrapolação podem ser implementadas de diferentes maneiras. Este guia amplifica o ruído com "gate folding digital", o que significa que as portas de dois qubits são substituídas por sequências equivalentes da porta e da sua inversa. Por exemplo, substituir um unitário UU por UU†UU U^† U resultaria em um fator de amplificação de ruído de 3. Você também pode implementar a amplificação com amplificação probabilística de erros (PEA), que exige uma etapa adicional de aprendizado de um modelo de ruído e é abordada no guia de amplificação probabilística de erros. Este guia extrapola usando algumas estratégias de ajuste prontas (linear e exponencial) e também demonstra como implementar sua própria função de ajuste personalizada.

Tenha em mente que, embora a ZNE possa ser eficaz para mitigar o ruído quântico com um custo de amostragem menor do que outros métodos, ela não oferece limites para o viés.

Gate folding

O fluxo de trabalho é o seguinte:

  1. Especificar um circuito e um observável
  2. Calcular valores esperados exatos usando o simulador de vetor de estado do Qiskit
  3. Calcular valores esperados ruidosos, sem mitigação
  4. Preparar um programa ZNE com gate folding digital e circuitos de amostra com escalas de ruído variadas
  5. Extrapolar para o limite de ruído zero com funções de ajuste prontas e personalizadas
  6. Comparar os valores esperados mitigados

Especificar um circuito e um observável​

Calcule a magnetização média por sítio ⟨Z⟩avg=1N∑i=1N⟨Zi⟩\langle Z \rangle_{\text{avg}} = \frac{1}{N} \sum_{i=1}^{N} \langle Z_i \rangle de um modelo de Ising de campo transversal (TFIM) trotterizado em oito qubits em uma topologia de anel. Você vai varrer vários valores da intensidade do campo transversal e plotar a magnetização em função da intensidade do campo.

# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util

_needed = {"IPython": "IPython", "matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_aer": "qiskit-aer", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "qiskit_mitigation": "qiskit-mitigation", "scipy": "scipy"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import warnings

import numpy as np
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.quantum_info import SparsePauliOp

warnings.filterwarnings("ignore")

def trotter_circuit(num_qubits, theta, layers, num_steps):
"""Trotterized transverse-field Ising evolution: RX on every site, then a CZ layer per coupling layer."""
circuit = QuantumCircuit(num_qubits)
for _ in range(num_steps):
circuit.rx(theta, range(num_qubits))
for layer in layers:
circuit.barrier()
for edge in layer:
circuit.cz(*edge)
circuit.sdg(edge)
circuit.barrier()
return circuit

num_qubits = 8
trotter_steps = 15
ring_coupling = [(i, (i + 1) % num_qubits) for i in range(num_qubits)]
layers = [
ring_coupling[0::2],
ring_coupling[1::2],
] # two disjoint layers of CZ gates
theta = Parameter("theta")
circuit = trotter_circuit(num_qubits, theta, layers, trotter_steps)

# Parameter values: theta in [0, 0.25 pi] - values for the transverse-field sweep
data_points = 6
parameter_values = np.linspace(0, 0.25 * np.pi, data_points).reshape(
(data_points, 1)
)

# Define observables - Z on each site
observables = [
SparsePauliOp("I" * (num_qubits - q - 1) + "Z" + "I" * q)
for q in range(num_qubits)
]

print("Visualization of 1 Trotter step")
trotter_circuit(num_qubits, theta, layers, 1).draw("mpl", scale=0.7)
Visualization of 1 Trotter step

Output of the previous code cell

Calcular valores esperados exatos usando o simulador de vetor de estado do Qiskit​

Use a classe Statevector do Qiskit para calcular o valor esperado exato de ZiZ_i em cada sítio para cada valor de θ\theta.

import matplotlib.pyplot as plt
from qiskit.quantum_info import Statevector

exact_evs = np.array(
[
[
Statevector(circuit.assign_parameters([value]))
.expectation_value(obs)
.real
for value in parameter_values.ravel()
]
for obs in observables
]
) # shape (observables, parameter values)

x_vals = parameter_values.ravel() / np.pi
fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(x_vals, exact_evs.mean(axis=0), "ko-", label="Exact")
ax.set_xlabel(r"$\theta/\pi$")
ax.set_ylabel(r"$\langle Z \rangle_{\text{avg}}$")
ax.legend()
plt.tight_layout()
plt.show()

Output of the previous code cell

Calcular valores esperados ruidosos, sem mitigação​

Um QuantumProgram contém um circuito quântico e um samplex em um formato adequado para a primitiva Executor. MitigationTask.prepare constrói um QuantumProgram a partir do circuito, dos observáveis e dos valores dos parâmetros, e envolve o circuito em caixas com as opções de boxing fornecidas. Você define a opção de boxing measure_annotations como "change_basis", que rotaciona as medições para as bases necessárias, e adiciona add_tags: "unique_box" para que o simulador possa mapear um modelo de ruído nas caixas do circuito. O programa passa por Pauli twirling por padrão, o que, em hardware, transforma ruído coerente no ruído de Pauli que os métodos de mitigação assumem. num_randomizations define quantas instâncias twirled de cada circuito são amostradas e shots_per_randomization define quantos shots cada instância recebe, então o produto dos dois é o orçamento de shots por circuito.

Um usuário com acesso a QPU executaria o programa com um Executor criado a partir de um Backend do QiskitRuntimeService. Aqui, você vai passar um AerSimulator para o Executor, que executa o programa localmente, e usar a opção de simulador layer_noise_model para injetar um modelo de ruído de Pauli-Lindblad aleatório em cada camada de portas emaranhadoras, gerado com random_pauli_lindblad_noise do qiskit-mitigation com um erro por porta em camada (EPLG) de 0.003, típico das QPUs Heron atuais.

from qiskit.circuit import CircuitInstruction
from qiskit.quantum_info import PauliLindbladMap, pauli_basis
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import Executor
from qiskit_mitigation import MitigationTask, find_combined_unique_layers

def generate_random_pauli_lindblad_noise(
layers,
num_qubits: int,
eplg: float = 3e-3,
seed: int | None = None,
) -> list[tuple[CircuitInstruction, PauliLindbladMap]]:
"""Generate random Pauli-Lindblad noise over the full Pauli basis."""
layer_noise_model = []
for layer in layers:
layer_instructions = layer.operation.params[0]
contain_measurement = any(
inst.name.startswith("measure") for inst in layer_instructions
)
if contain_measurement: # do not add measurement noise
layer_noise = PauliLindbladMap.identity(num_qubits)
else:
edges = [
(inst.qubits[0]._index, inst.qubits[1]._index)
for inst in layer_instructions
] # extract the edges of the layer

basis_paulis = [p for p in pauli_basis(2) if np.sum(p.x + p.z)]
rng = np.random.default_rng(seed=seed)
num_generators = len(basis_paulis)
rates = rng.random(len(basis_paulis)) * eplg / num_generators

layer_noise = PauliLindbladMap.from_sparse_list(
[
(pauli.to_label(), edge, rate)
for pauli, rate in zip(basis_paulis, rates, strict=True)
for edge in edges
],
num_qubits=num_qubits,
)
layer_noise_model.append((layer, layer_noise))
return layer_noise_model

num_randomizations = 50
shots_per_randomization = 64

boxing_options = {
"measure_annotations": "change_basis",
"add_tags": "unique_box",
}

# find the unique layers to generate the layered noise for
box_layers = find_combined_unique_layers(
[circuit], custom_boxing_options=boxing_options
)
layer_noise_model = generate_random_pauli_lindblad_noise(
box_layers, num_qubits, seed=42
)

# a local noisy simulator in place of a QPU backend
noisy_executor = Executor(
AerSimulator(),
options={"simulator": {"layer_noise_model": layer_noise_model}},
)

task = MitigationTask()
program = task.prepare(
circuit=circuit,
observables=observables,
parameters=parameter_values,
num_randomizations=num_randomizations,
shots_per_randomization=shots_per_randomization,
custom_boxing_options=boxing_options,
)
program._semantic_role = "simulation" # flag the program for the simulator
unmitigated_res = task.postprocess(noisy_executor.run(program).result())

fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(x_vals, exact_evs.mean(axis=0), "k-", label="Exact")
ax.errorbar(
x_vals,
np.mean(unmitigated_res.data.evs, axis=0),
np.mean(unmitigated_res.data.stds, axis=0),
fmt="o-",
label="Unmitigated",
)
ax.set_xlabel(r"$\theta/\pi$")
ax.set_ylabel(r"$\langle Z \rangle_{\text{avg}}$")
ax.legend()
plt.tight_layout()
plt.show()

Output of the previous code cell

Preparar um programa ZNE com gate folding digital e circuitos de amostra com escalas de ruído variadas​

Prepare um programa ZNE contendo circuitos com escalas de ruído crescentes. Cada fator de ruído representa o múltiplo de portas emaranhadoras que o circuito contém (um fator de ruído de 3.0 tem três vezes mais portas emaranhadoras que o circuito original). O circuito de cada fator de ruído passa por twirling com o mesmo número de randomizações e shots do programa sem mitigação.

from qiskit_mitigation import GateFolding

task_gf = GateFolding()
program_zne = task_gf.prepare(
circuit=circuit,
observables=observables,
parameters=parameter_values,
num_randomizations=num_randomizations,
shots_per_randomization=shots_per_randomization,
custom_boxing_options=boxing_options,
noise_factors=[1, 3, 5], # ZNE option
)

Desenhe o primeiro passo de Trotter do template com twirling com fator de ruído 1 e com fator de ruído 3 para visualizar a amplificação. Para o fator de ruído 3.0, cada camada do circuito é substituída pela camada, sua inversa e a camada novamente.

import IPython.display

def first_trotter_step(template, noise_factor):
"""The template up to the closing barrier of its first Trotter step (two layers, each folded ``noise_factor`` times)."""
toy = template.copy_empty_like()
for instruction in template.data:
toy.append(instruction)
if (
instruction.operation.name == "barrier"
and instruction.operation.label.startswith(
f"R{2 * noise_factor - 1}@"
)
):
break
return toy

IPython.display.display(
first_trotter_step(program_zne.items[0].circuit, 1).draw(
"mpl", scale=0.7, plot_barriers=False
)
)
first_trotter_step(program_zne.items[1].circuit, 3).draw(
"mpl", fold=-1, plot_barriers=False
)

Output of the previous code cell

Output of the previous code cell

Por fim, execute o programa no simulador ruidoso. Os resultados alimentam a extrapolação na próxima etapa.

program_zne._semantic_role = (
"simulation" # flag the program for the simulator
)
job_zne_results = noisy_executor.run(program_zne).result()

Extrapolar para o limite de ruído zero com funções de ajuste prontas e personalizadas​

Agora que você tem dados para cada escala de ruído, pode ajustar uma função aos dados e extrapolar de volta para o valor de ruído zero, para estimar um valor esperado do observável livre de ruído. Há muitas maneiras de ajustar dados, e a escolha certa da função depende da natureza dos dados amostrados. GateFolding.postprocess oferece várias funções de extrapolação prontas, incluindo linear, exponential e double_exponential. O método postprocess recebe uma lista de métodos de extrapolação, avalia-os em ordem e retorna o primeiro valor fisicamente válido e com variância finita. Isso significa que a ordem em que você especifica os extrapoladores tem grande impacto sobre qual extrapolador é escolhido. Informar fallback como extrapolador significa que, se nenhuma resposta válida for encontrada, o valor esperado do menor fator de ruído será escolhido e retornado. Se fallback não for informado e nenhum extrapolador produzir um valor fisicamente válido, o resultado com o menor std é retornado. Sob ruído de Pauli, um observável decai exponencialmente com o fator de ruído, então, com 30 camadas (15 passos de Trotter, 2 camadas por passo) de portas emaranhadoras, o extrapolador exponencial é a escolha natural; você também pode extrapolar linearmente, o que é uma escolha comum quando o decaimento é pequeno.

Para usar uma função de ajuste personalizada, passe o extrapolador "custom" junto com uma tupla custom_fit contendo a função de ajuste, uma lista com um palpite inicial para cada um dos seus parâmetros e os limites. Eles são repassados para scipy.optimize.curve_fit. O pós-processamento pode ser repetido com diferentes extrapoladores sem reexecutar o programa. Aqui você usa uma função de ajuste personalizada de raiz quadrada. Para visualizar a extrapolação, plote a magnetização média por sítio calculada em cada fator de ruído para θ=0.15π\theta = 0.15\pi, os três ajustes e a média por sítio dos valores esperados extrapolados no ponto de ruído zero. Os três pontos de dados parecem quase lineares, mas a extrapolação linear fica visivelmente abaixo do valor exato. Como previsto, o ajuste exponencial é o que estima com mais eficácia o valor esperado sem ruído.

from scipy.optimize import curve_fit

zne_mitigated_exp = task_gf.postprocess(
job_zne_results, extrapolator=["exponential", "linear"]
)
zne_mitigated_lin = task_gf.postprocess(
job_zne_results, extrapolator=["linear"]
)

def sqrt_fit(x, a, b):
return a * np.sqrt(x) + b

custom_fit = (sqrt_fit, [1.0, 1.0], (-np.inf, np.inf))
zne_mitigated_custom = task_gf.postprocess(
job_zne_results,
extrapolator=["custom", "exponential", "linear"],
custom_fit=custom_fit,
)

# The result holds the expectation value at each noise factor and the extrapolated value, but
# not the fit parameters, so we refit the same models here to draw the curves.
noise_factors = np.array([1, 3, 5])
angle = 3 # theta = 0.15 pi, the angle with the largest unmitigated error
amplifications = zne_mitigated_exp.data.evs_noise_factors[:, angle, :].mean(
axis=0
)
x_fit = np.linspace(0, 5, 51)
fits = {
"exponential": (
lambda x, a, b: a * np.exp(b * x),
zne_mitigated_exp,
"black",
),
"linear": (lambda x, a, b: a * x + b, zne_mitigated_lin, "blue"),
"square-root": (sqrt_fit, zne_mitigated_custom, "green"),
}

plt.plot(
noise_factors, amplifications, "o", color="red", label="amplifications"
)
for name, (fit, res, color) in fits.items():
params, _ = curve_fit(fit, noise_factors, amplifications, p0=[1.0, -0.1])
plt.plot(
x_fit, fit(x_fit, *params), "--", color=color, label=f"{name} fit"
)
plt.plot(
[0],
res.data.evs[:, angle].mean(),
"*",
color=color,
markersize=15,
label=f"{name} extrapolation",
)
plt.plot(
[0],
exact_evs[:, angle].mean(),
"_",
color="red",
markersize=15,
markeredgewidth=3,
label="exact",
)
plt.xlabel("noise factor")
plt.ylabel(r"$\langle Z \rangle_{\text{avg}}$")
plt.legend()
plt.show()

Output of the previous code cell

Comparar os valores esperados mitigados​

Plote a magnetização média por sítio de cada método ao longo da varredura de ângulos de rotação de Ising. A extrapolação exponencial recupera a magnetização exata dentro das suas barras de erro, enquanto a extrapolação linear remove boa parte do viés, mas consistentemente subestima, porque o decaimento verdadeiro é exponencial. O ajuste de raiz quadrada superestima muito.

results = {
"Unmitigated": unmitigated_res,
"ZNE exponential": zne_mitigated_exp,
"ZNE linear": zne_mitigated_lin,
"ZNE square-root": zne_mitigated_custom,
}

fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(x_vals, exact_evs.mean(axis=0), "k-", label="Exact")
for label, res in results.items():
ax.errorbar(
x_vals,
np.mean(res.data.evs, axis=0),
np.mean(res.data.stds, axis=0),
fmt="o-",
label=label,
)
ax.set_xlabel(r"$\theta/\pi$")
ax.set_ylabel(r"$\langle Z \rangle_{\text{avg}}$")
ax.legend()
ax.set_title(
f"Error-Mitigated Magnetization [{num_qubits}-qubit, {trotter_steps} Trotter steps]"
)
plt.tight_layout()
plt.show()

Output of the previous code cell