Перейти до основного вмісту
Цей проєкт у бета-версії. Знайшов(-ла) помилку? Відкрити issue на GitHub. Є запитання чи ідея? Запитати в Discussions — будемо раді твоєму відгуку!
Яка якість перекладу на цій сторінці?

Екстраполяція до нульового шуму зі згортанням вентилів та власною екстраполяцією

Екстраполяція до нульового шуму (ZNE) — це техніка пом'якшення квантового шуму в обчисленнях очікуваних значень спостережуваних. Вона працює шляхом виконання квантової схеми при зростаючих масштабах шуму, взяття очікуваних значень для цих масштабів і екстраполяції для оцінки очікуваного значення при нульовому шумі. Як підсилення шуму, так і етапи екстраполяції можна реалізувати по-різному. Цей посібник підсилює шум за допомогою «цифрового згортання вентилів», що означає заміну двокубітних вентилів еквівалентними послідовностями вентиля та його оберненого. Наприклад, заміна унітарного оператора UU на UU†UU U^† U дає коефіцієнт підсилення шуму 3. Підсилення також можна реалізувати за допомогою ймовірнісного підсилення помилок (PEA), що потребує додаткового кроку навчання моделі шуму і розглянуте в посібнику з ймовірнісного підсилення помилок. Цей посібник виконує екстраполяцію за допомогою кількох вбудованих стратегій апроксимації (лінійної та експоненціальної), а також показує, як реалізувати власну функцію апроксимації.

Пам'ятай: хоча ZNE може ефективно пом'якшувати квантовий шум за нижчої вартості вибірки, ніж інші методи, вона не дає жодних меж для зміщення.

Gate folding

Робочий процес такий:

  1. Задати схему та спостережувану
  2. Обчислити точні очікувані значення за допомогою симулятора вектора стану Qiskit
  3. Обчислити зашумлені непом'якшені очікувані значення
  4. Підготувати програму ZNE з цифровим згортанням вентилів та вибіркою схем різних масштабів шуму
  5. Екстраполювати до межі нульового шуму за допомогою вбудованих і власних функцій апроксимації
  6. Порівняти пом'якшені очікувані значення

Задай схему та спостережувану​

Обчисли усереднену за вузлами намагніченість ⟨Z⟩avg=1N∑i=1N⟨Zi⟩\langle Z \rangle_{\text{avg}} = \frac{1}{N} \sum_{i=1}^{N} \langle Z_i \rangle тротеризованої моделі Ізинга з поперечним полем (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

Output of the previous code cell

Обчисли точні очікувані значення за допомогою симулятора вектора стану Qiskit​

Використай клас Qiskit Statevector, щоб обчислити точне очікуване значення ZiZ_i на кожному вузлі для кожного значення θ\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

Обчисли зашумлені непом'якшені очікувані значення​

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

Output of the previous code cell

Підготуй програму 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
)

Output of the previous code cell

Output of the previous code cell

Нарешті, запусти програму на зашумленому симуляторі. Його результати використовуються для екстраполяції на наступному кроці.

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. Постобробку можна повторювати з різними екстраполяторами без повторного виконання програми. Тут ти використовуєш власну апроксимаційну функцію квадратного кореня. Щоб візуалізувати екстраполяцію, побудуй графік усередненої за вузлами намагніченості, обчисленої для кожного коефіцієнта шуму при θ=0.15π\theta = 0.15\pi, трьох апроксимацій та усередненого за вузлами екстрапольованого очікуваного значення в точці нульового шуму. Три точки даних виглядають майже лінійними, проте лінійна екстраполяція потрапляє помітно нижче за точне значення. Як і передбачалося, експоненціальна апроксимація найефективніше оцінює очікуване значення без шуму.

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

Порівняй пом'якшені очікувані значення​

Побудуй графік усередненої за вузлами намагніченості для кожного методу по всьому діапазону кутів обертання Ізинга. Експоненціальна екстраполяція відновлює точну намагніченість у межах своїх похибок, тоді як лінійна екстраполяція усуває значну частину зміщення, але постійно недооцінює, бо справжній спад експоненціальний. Апроксимація квадратним коренем сильно переоцінює.

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