Екстраполяція до нульового шуму зі згортанням вентилів та власною екстраполяцією
Екстраполяція до нульового шуму (ZNE) — це техніка пом'якшення квантового шуму в обчисленнях очікуваних значень спостережуваних. Вона працює шляхом виконання квантової схеми при зростаючих масштабах шуму, взяття очікуваних значень для цих масштабів і екстраполяції для оцінки очікуваного значення при нульовому шумі. Як підсилення шуму, так і етапи екстраполяції можна реалізувати по-різному. Цей посібник підсилює шум за допомогою «цифрового згортання вентилів», що означає заміну двокубітних вентилів еквівалентними послідовностями вентиля та його оберненого. Наприклад, заміна унітарного оператора на дає коефіцієнт підсилення шуму 3. Підсилення також можна реалізувати за допомогою ймовірнісного підсилення помилок (PEA), що потребує додаткового кроку навчання моделі шуму і розглянуте в посібнику з ймовірнісного підсилення помилок. Цей посібник виконує екстраполяцію за допомогою кількох вбудованих стратегій апроксимації (лінійної та експоненціальної), а також показує, як реалізувати власну функцію апроксимації.
Пам'ятай: хоча ZNE може ефективно пом'якшувати квантовий шум за нижчої вартості вибірки, ніж інші методи, вона не дає жодних меж для зміщення.
Робочий процес такий:
- Задати схему та спостережувану
- Обчислити точні очікувані значення за допомогою симулятора вектора стану Qiskit
- Обчислити зашумлені непом'якшені очікувані значення
- Підготувати програму ZNE з цифровим згортанням вентилів та вибіркою схем різних масштабів шуму
- Екстраполювати до межі нульового шуму за допомогою вбудованих і власних функцій апроксимації
- Порівняти пом'якшені очікувані значення
Задай схему та спостережувану
Обчисли усереднену за вузлами намагніченість тротеризованої моделі Ізинга з поперечним полем (TFIM) на восьми кубітах у кільцевій топології. Ти переберешеш кілька значень напруженості поперечного поля та побудуєш графік намагніченості як функції напруженості поля.
# 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
Обчисли точні очікувані значення за допомогою симулятора вектора стану Qiskit
Використай клас Qiskit Statevector, щоб обчислити точне очікуване значення на кожному вузлі для кожного значення .
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()
Обчисли зашумлені непом'якшені очікувані значення
QuantumProgram містить квантову схему та samplex у форматі, придатному для примітива Executor. MitigationTask.prepare будує QuantumProgram зі схеми, спостережуваних і значень параметрів та огортає схему в бокси із заданими параметрами боксування. Ти задаєш параметр боксування measure_annotations рівним "change_basis", що повертає вимірювання в потрібні базиси, і додаєш add_tags: "unique_box", щоб симулятор міг накласти модель шуму на бокси схеми. Програма за замовчуванням піддається паулівському твірлінгу, що на апаратному забезпеченні перетворює когерентний шум на паулівський шум, який передбачають методи пом'якшення. num_randomizations задає, скільки твірлованих екземплярів кожної схеми береться у вибірку, а shots_per_randomization задає, скільки шотів отримує кожен екземпляр, тож їх добуток — це бюджет шотів на схему.
Користувач з доступом до QPU виконав би програму за допомогою Executor, побудованого з бекенда QiskitRuntimeService. Тут ти передаси AerSimulator в Executor, який виконує програму локально, і використаєш його опцію симулятора layer_noise_model, щоб ввести випадкову модель шуму Паулі-Ліндблада на кожен шар заплутуючих вентилів, згенеровану за допомогою random_pauli_lindblad_noise з qiskit-mitigation із помилкою на шаруватий вентиль (EPLG) 0.003, типовою для сучасних QPU Heron.
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()
Підготуй програму ZNE зі згортанням вентилів та вибіркою схем різних масштабів шуму
Підготуй програму ZNE зі схемами зростаючих масштабів шуму. Кожен коефіцієнт шуму відповідає кратності заплутуючих вентилів, які містить схема (коефіцієнт шуму 3.0 дає втричі більше заплутуючих вентилів, ніж в оригінальній схемі). Схема для кожного коефіцієнта шуму твірлується з тією самою кількістю рандомізацій і шотів, що й непом'якшена програма.
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
)
Зобрази перший крок Троттера твірлованого шаблону з коефіцієнтом шуму 1 та з коефіцієнтом шуму 3, щоб візуалізувати підсилення. Для коефіцієнта шуму 3.0 кожен шар схеми замінюється шаром, його оберненим і знову шаром.
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
)

Нарешті, запусти програму на зашумленому симуляторі. Його результати використовуються для екстраполяції на наступному кроці.
program_zne._semantic_role = (
"simulation" # flag the program for the simulator
)
job_zne_results = noisy_executor.run(program_zne).result()
Екстраполюй до межі нульового шуму за допомогою вбудованих і власних функцій апроксимації
Тепер, коли в тебе є дані для кожного масштабу шуму, ти можеш апроксимувати їх функцією та екстраполювати назад до значення при нульовому шумі, щоб оцінити очікуване значення спостережуваної без шуму. Існує багато способів апроксимації даних, і правильний вибір функції залежить від природи даних, що вибираються. GateFolding.postprocess надає кілька вбудованих функцій екстраполяції, зокрема linear, exponential і double_exponential. Метод postprocess приймає список методів екстраполяції, оцінює їх по черзі й повертає перше значення з фізично коректними величинами та скінченною дисперсією. Це означає, що порядок, у якому ти задаєш екстраполятори, суттєво впливає на те, який екстраполятор буде обрано. Передача fallback як екстраполятора означає, що якщо коректних відповідей не знайдено, буде обрано й повернуто очікуване значення для найменшого коефіцієнта шуму. Якщо fallback не передано і жоден екстраполятор не дає фізично коректного значення, повертається результат із найменшим std. За паулівського шуму спостережувана спадає експоненціально з коефіцієнтом шуму, тож для 30 шарів (15 кроків Троттера, 2 шари на крок) заплутуючих вентилів природним вибором є експоненціальний екстраполятор; також можна екстраполювати лінійно, що є поширеним вибором, коли спад малий.
Щоб використати власну функцію апроксимації, передай екстраполятор "custom" разом із кортежем custom_fit, що містить функцію апроксимації, список із початковим наближенням для кожного з її параметрів та межі. Вони передаються в scipy.optimize.curve_fit. Постобробку можна повторювати з різними екстраполяторами без повторного виконання програми. Тут ти використовуєш власну апроксимаційну функцію квадратного кореня. Щоб візуалізувати екстраполяцію, побудуй графік усередненої за вузлами намагніченості, обчисленої для кожного коефіцієнта шуму при , трьох апроксимацій та усередненого за вузлами екстрапольованого очікуваного значення в точці нульового шуму. Три точки даних виглядають майже лінійними, проте лінійна екстраполяція потрапляє помітно нижче за точне значення. Як і передбачалося, експоненціальна апроксимація найефективніше оцінює очікуване значення без шуму.
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()

Порівняй пом'якшені очікувані значення
Побудуй графік усередненої за вузлами намагніченості для кожного методу по всьому діапазону кутів обертання Ізинга. Експоненціальна екстраполяція відновлює точну намагніченість у межах своїх похибок, тоді як лінійна екстраполяція усуває значну частину зміщення, але постійно недооцінює, бо справжній спад експоненціальний. Апроксимація квадратним коренем сильно переоцінює.
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()
