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 por 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.
O fluxo de trabalho é o seguinte:
- Especificar um circuito e um observável
- Calcular valores esperados exatos usando o simulador de vetor de estado do Qiskit
- Calcular valores esperados ruidosos, sem mitigação
- Preparar um programa ZNE com gate folding digital e circuitos de amostra com escalas de ruído variadas
- Extrapolar para o limite de ruído zero com funções de ajuste prontas e personalizadas
- Comparar os valores esperados mitigados
Especificar um circuito e um observável
Calcule a magnetização média por sítio 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
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 em cada sítio para cada valor de .
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()
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()
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
)

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

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