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

Покращення очікуваних значень за допомогою поглинання поширеного шуму (PNA)

Оцінка використання: 10 хвилин на процесорі Heron (ПРИМІТКА: Це лише оцінка. Ваш час виконання може відрізнятися.)

Результати навчання​

  • Що таке поглинання поширеного шуму (PNA) і як воно пом'якшує шум двокубітних гейтів шляхом поглинання вивчених обернених каналів шуму у вимірювану спостережувану величину

  • Як використовувати samplomatic для боксування та анотування шарів схеми для твірлінгу, змін базису та введення шуму

  • Як вивчати шум шарів за допомогою [NoiseLearnerV3](/guides/directed-execution-model#noiselearnerv3) і поширювати його у спостережуване, що пом'якшує шум, за допомогою qiskit-addon-pna

  • Як вибирати рандомізовані схеми за допомогою класів QuantumProgram та Executor у qiskit-ibm-runtime і поєднувати PNA з TREX та пост-селекцією

Передумови​

Теоретичні відомості​

У цьому посібнику ми демонструємо, як використовувати передові інструменти пом'якшення помилок у Qiskit для покращення оцінки очікуваного значення в зашумлених квантових експериментах.

Що таке поглинання поширеного шуму (PNA)?​

Поглинання поширеного шуму — це техніка пом'якшення помилок гейтів шляхом поширення спостережуваного через обернений канал шуму, що впливає на двокубітні гейти, у результаті чого отримується спостережуване, що пом'якшує шум.

Ми можемо використовувати пом'якшення квантових помилок для отримання корисних очікуваних значень із зашумленого квантового апаратного забезпечення без потреби в повній відмовостійкості. PNA спеціально зосереджується на поглинанні ефектів шуму в саме спостережуване, а не на модифікації операцій схеми.

Кожен зашумлений гейт у квантовій схемі можна змоделювати як ідеальний гейт, за яким слідує канал шуму. PNA вивчає або характеризує ці канали шуму та визначає їхні обернені. Замість вставки обернених операцій у виконання на апаратному забезпеченні (що зазвичай нездійсненно), PNA поширює обернені канали шуму вперед через схему та застосовує їх до спостережуваного. Цей процес перетворює спостережуване OO на новий оператор O~\tilde{O}, такий що вимірювання O~\tilde{O} на зашумленій схемі дає те саме очікуване значення, що й вимірювання OO на ідеальній, безшумовій схемі.

Це можна підсумувати так:

  1. Змоделювати кожен зашумлений гейт uiu_i як ідеальну операцію, за якою слідує канал шуму Λi\Lambda_i.
  2. Вивчити або оцінити кожен Λi\Lambda_i за допомогою інструментів характеризації шуму.
  3. Визначити та поширити обернені карти шуму Λi−1\Lambda_i^{-1} вперед через схему за допомогою технік передачі Паулі.
  4. Поглинути ці обернені в спостережуване, що дає пом'якшений щодо шуму оператор O~\tilde{O}.

Коли шум описується як канал Паулі (або, загальніше, як розріджений канал Паулі-Ліндблада), це поширення можна виконати ефективно за допомогою поширення Паулі. Поширення Паулі забезпечує основу для апроксимації того, як обернені канали шуму трансформуються, проходячи через шари кліффордових та некліффордових операцій, при цьому контролюючи обчислювальну складність.

Зміщуючи пом'якшення в спостережуване, PNA уникає великих накладних витрат на вибірку, які інакше виникли б через вставку фізичних коригувальних операцій у схему. Натомість виконується оригінальна зашумлена схема, тоді як спостережуване перетворюється на новий оператор O~\tilde{O}, очікуване значення якого скасовує ефекти шуму.

Робочий процес PNA​

Процес можна зрозуміти через наступні концептуальні етапи. Перша схема ілюструє стандартний зашумлений експеримент.

Noisy experiment

Якщо ми вивчимо модель шуму, ми можемо застосувати її обернену і скасувати шум.

Noise-mitigated experiment

Замість реалізації оберненого каналу шуму шляхом вибірки його на QPU, як у імовірнісному скасуванні помилок (PEC), ми застосовуємо його класично до виміряного спостережуваного за допомогою поширення Паулі. Отримане спостережуване ефективно пом'якшує вивчений шум гейта при вимірюванні.

PNA overview

Модульне пом'якшення помилок за допомогою Samplomatic та Executor​

Підхід PNA будується на модульній архітектурі пом'якшення помилок у Qiskit. Ця архітектура використовує бібліотеку samplomatic разом із класами QuantumProgram та Executor (доданими до Qiskit Runtime в qiskit-ibm-runtime v0.47.0), щоб зробити такі техніки, як поглинання поширеного шуму та твірлінг Паулі, композиційними та придатними для повторного використання в різних експериментах.

Замість вбудовування логіки пом'якшення в саме визначення схеми, пом'якшення виражається декларативно через анотації samplomatic і керується програмно через Executor, який контролює, як генеруються, виконуються та постобробляються рандомізовані схеми.

У цьому посібнику ми реалізуємо патерн Qiskit, щоб продемонструвати, як PNA може поширювати обернені канали шуму Паулі та відповідно модифікувати спостережуване, щоб покращити оцінку очікуваних значень на зашумлених QPU.

Огляд робочого процесу​

  • Крок 1: Відображення на квантову задачу

    • Побудова дзеркальної тротеризованої моделі Ізінга з поштовхом і цільового спостережуваного.
  • Крок 2: Характеризація та поширення шуму

    • Використання samplomatic для ідентифікації та анотування унікальних двокубітних шарів і вимірювань у схемі.

    • Вивчення шуму, що впливає на кожен унікальний шар, за допомогою NoiseLearnerV3.

    • Відображення кожної анотації InjectNoise на відповідну вивчену модель шуму.

    • Використання qiskit-addon-pna для поширення обернених каналів шуму вперед через схему та поглинання їх у цільове спостережуване.

  • Крок 3: Виконання квантових експериментів

    • Визначення QuantumProgram для вказівки рандомізованої вибірки через samplex та виконання експериментів на бекенді за допомогою Executor.
  • Крок 4: Реконструкція та аналіз результатів

    • Порівняння стратегій пом'якшення (PNA, PNA+TREX, PNA+PS, PNA+PS+TREX) та візуалізація покращення порівняно з непом'якшеними результатами.

Вимоги​

Перед початком цього посібника переконайтеся, що у вас встановлено наступне:

  • Qiskit SDK v2.2 або новіший, з підтримкою візуалізації

  • Qiskit Runtime v0.47 або новіша (pip install qiskit-ibm-runtime)

  • Samplomatic v0.13 або новіша (pip install samplomatic)

  • Аддон PNA для Qiskit (pip install qiskit-addon-pna)

  • Утиліти аддону Qiskit (pip install qiskit-addon-utils)

Налаштування​

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

_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_pna": "qiskit-addon-pna", "qiskit_addon_utils": "qiskit-addon-utils", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "samplomatic": "samplomatic"}
_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")
from qiskit import QuantumCircuit
from qiskit.quantum_info import Pauli, SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager, PassManager
from qiskit_ibm_runtime import (
QiskitRuntimeService,
QuantumProgram,
Executor,
NoiseLearnerV3,
)
from qiskit_addon_utils.exp_vals.measurement_bases import (
get_measurement_bases,
)
from qiskit_addon_utils.exp_vals.expectation_values import (
executor_expectation_values,
)
from qiskit_addon_utils.noise_management import trex_factors
from qiskit_addon_utils.noise_management.post_selection import PostSelector
from qiskit_addon_utils.noise_management.post_selection.transpiler.passes import (
AddPostSelectionMeasures,
AddSpectatorMeasures,
)
from qiskit_addon_pna import generate_noise_mitigating_observable
import samplomatic
from samplomatic.transpiler import generate_boxing_pass_manager
from samplomatic.annotations import InjectNoise
from samplomatic.utils import get_annotation, find_unique_box_instructions
import numpy as np
import matplotlib.pyplot as plt

# Selects a connected chain of low-error qubits on the target backend.
# The mirrored kicked-Ising circuit is a 1D chain, so we only need a line
# of connected physical qubits; the helper walks the backend's coupling map
# and grows a chain along the lowest-error two-qubit edges, so it works for
# any backend rather than relying on a hardcoded layout.
def find_qubit_chain(backend, length):
"""Find a connected chain of ``length`` physical qubits on ``backend``.

The chain is grown greedily along the lowest-error two-qubit edges, so it
favors better-performing qubits. Because the mirrored kicked-Ising circuit
is a 1D chain, a connected line is all we need.
"""
target = backend.target

# Identify the native two-qubit gate and build a per-edge error lookup.
two_qubit_gate = next(
name
for name in target.operation_names
if target[name]
and all(q is not None and len(q) == 2 for q in target[name])
)
edge_error = {
frozenset(qargs): (
1.0 if props is None or props.error is None else props.error
)
for qargs, props in target[two_qubit_gate].items()
}

graph = backend.coupling_map.graph.to_undirected(multigraph=False)
neighbors = {n: list(graph.neighbors(n)) for n in graph.node_indices()}

def first_chain_from(start):
path, visited = [start], {start}

def grow():
if len(path) == length:
return True
node = path[-1]
order = sorted(
neighbors[node],
key=lambda m: edge_error.get(frozenset((node, m)), 1.0),
)
for nxt in order:
if nxt not in visited:
visited.add(nxt)
path.append(nxt)
if grow():
return True
path.pop()
visited.remove(nxt)
return False

return path if grow() else None

def chain_cost(path):
return sum(
edge_error.get(frozenset((path[i], path[i + 1])), 1.0)
for i in range(len(path) - 1)
)

# Try low-degree qubits first (the natural ends of long chains) and keep
# the lowest-error chain found.
best_path, best_cost = None, float("inf")
for start in sorted(neighbors, key=lambda n: len(neighbors[n])):
chain = first_chain_from(start)
if chain is not None and (cost := chain_cost(chain)) < best_cost:
best_path, best_cost = chain, cost

if best_path is None:
raise ValueError(
f"Could not find a connected chain of {length} qubits "
f"on '{backend.name}'."
)
return best_path

Приклад малого масштабу на симуляторі​

PNA пом'якшує фізичний шум двокубітних гейтів конкретного квантового процесора. Робочий процес залежить від двох апаратних сервісів, які не мають значного аналога на ідеальному симуляторі:

  • NoiseLearnerV3 експериментально характеризує розріджений канал шуму Паулі-Ліндблада, приєднаний до кожного унікального двокубітного шару транспільованої схеми. На безшумному симуляторі немає шуму для вивчення, і поширений обернений канал був би тотожним.

  • Примітив Executor семплує закручені (twirled), рандомізовані схеми, згенеровані samplomatic, на бекенді.

У принципі, ти можеш підставити синтетичну модель шуму. Наприклад, ти можеш приєднати інструкції PauliLindbladError за допомогою Qiskit Aer і передати отриману шумну схему безпосередньо до generate_noise_mitigating_observable. Однак це лише перевіряє класичний облік проти шуму, який ти сам ввів, і затьмарює суть техніки. З цієї причини ми пропускаємо приклад із симулятором і демонструємо повний робочий процес PNA безпосередньо на апаратному забезпеченні, з розбивкою кожного кроку шаблону Qiskit нижче.

Приклад великого масштабу на апаратному забезпеченні​

Тепер ми запускаємо повний робочий процес PNA для 30-вузлової моделі Ізінга з поштовхами (kicked Ising), виконаної на апаратному забезпеченні IBM Quantum®, дотримуючись чотирьох кроків шаблону Qiskit.

Крок 1: Відображення на квантову задачу​

Генерування дзеркальної схеми Тротера та спостережуваної величини

У цьому експерименті ми вивчатимемо часову динаміку 30-вузлової моделі Ізінга з поштовхами на 1D спіновому ланцюжку. Розглядуваний гамільтоніан:

H=−J∑⟨i,j⟩ZiZj+h∑iXiH = -J\sum\limits_{\langle i,j \rangle} Z_iZ_j + h\sum\limits_iX_i,

де J>0J>0 описує зв'язок найближчих сусідніх спінів, i<ji<j, а глобальне поперечне поле, hh, встановлено на π8\frac{\pi}{8}. Чим далі hh від кута Кліффорда (тобто, θ=nπ2,n∈Z\theta=n\frac{\pi}{2}, n \in \mathbb{Z}), тим складніше поширювати генератори анти-шуму через схему.

Для вибору спостережуваної величини ми розглянемо середню намагніченість одного вузла, 1N∑i=1N⟨zi⟩\frac{1}{N} \sum_{i=1}^{N} \langle z_i \rangle, де NN — кількість вузлів.

num_qubits = 30
num_trotter_steps = 10
rx_angle = np.pi / 8

# Avg single-site magnetization
id_pauli = Pauli("I" * num_qubits)
observable = (
SparsePauliOp([id_pauli.dot(Pauli("Z"), [i]) for i in range(num_qubits)])
/ num_qubits
)

# Implement Trotterized kicked-Ising model
circuit = QuantumCircuit(num_qubits)
for _step in range(num_trotter_steps):
circuit.rx(rx_angle, range(num_qubits))
for first_qubit in (1, 2):
for idx in range(first_qubit, num_qubits, 2):
# equivalent to Rzz(-pi/2):
circuit.sdg([idx - 1, idx])
circuit.cz(idx - 1, idx)
# Append the inverse circuit to complete the mirroring
circuit.compose(circuit.inverse(), inplace=True)
circuit.measure_active()
circuit.draw("mpl", fold=-1)

Output of the previous code cell

Крок 2: Оптимізація задачі для виконання на апаратному забезпеченні​

Наступний крок — підготувати нашу дзеркальну схему Тротера для виконання на реальному апаратному забезпеченні IBM Quantum. Запуск на QPU вимагає більшого, ніж просто побудова абстрактної схеми, оскільки нам потрібно оптимізувати її так, щоб:

  1. Вона відповідала нативному набору гейтів та зв'язності бекенда. Транспіляція відображає логічну схему в ISA-схему, яку підтримує цільовий бекенд. Це гарантує, що кожен гейт і взаємодія кубітів фізично реалізовні.

  2. Ми могли характеризувати шум на рівні шарів схеми. PNA покладається на вивчення та поширення обернених каналів шуму. Щоб зробити це ефективно, ми розбиваємо транспільовану схему на унікальні двокубітні "боксовані" шари. Ці бокси дозволяють нам асоціювати кожен шар схеми з власною вивченою моделлю шуму.

  3. Ми могли передати реалістичні моделі шуму в PNA. Після боксування схеми ми використовуємо сервіс NoiseLearnerV3, щоб експериментально вивчити канали шуму Паулі, що впливають на кожен унікальний двокубітний шар. Ці вивчені моделі потім прив'язуються назад до схеми через анотації Samplomatic.

Загалом, цей крок з'єднує розрив між ідеалізованою дзеркальною схемою та готовою для апаратного забезпечення схемою з вивченими моделями шуму. З таким налаштуванням PNA може поширювати обернені канали шуму через схему та відповідно коригувати спостережувану величину.

Підключення до бекенда та транспіляція в ISA-схему​

Спочатку ми ініціалізуємо IBM Quantum Compute Service і вибираємо бекенд. Автентифікуйся зі своїм власним обліковим записом, дотримуючись інструкцій щодо збереження облікових даних, після чого QiskitRuntimeService() автоматично їх підхопить.

# Initialize IBM Quantum Compute Service using your saved credentials
service = QiskitRuntimeService()

backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=num_qubits
)
# Re-fetch with fractional gates enabled (least_busy does not forward this)
# Fractional gates are enabled so the non-Clifford Rx rotations are supported natively.
backend = service.backend(backend.name, use_fractional_gates=True)
print(f"Selected backend: {backend.name}")
Selected backend: ibm_fez

Далі ми обираємо з'єднаний ланцюжок кубітів на бекенді та транспілюємо схему на нього.

Використовуючи допоміжну функцію find_qubit_chain, визначену в розділі Налаштування, ми обираємо лінію з num_qubits з'єднаних фізичних кубітів. Потім ми транспілюємо з optimization_level=0 і закріплюємо цей ланцюжок як initial_layout, що точно зберігає структуру шарів двокубітних гейтів дзеркальної схеми. Саме на цій структурі покладаються кроки боксування та вивчення шуму, тому вищого рівня оптимізації (який би скасував дзеркальні гейти) тут слід уникати.

# Find a connected, low-error chain of qubits on the chosen backend
layout = find_qubit_chain(backend, num_qubits)

# Transpile the circuit for the target backend, pinning the chain as the layout.
# optimization_level=0 preserves the mirrored two-qubit-gate layers that the
# boxing and noise-learning steps rely on.
pm = generate_preset_pass_manager(
backend=backend, optimization_level=0, initial_layout=layout
)
isa_circuit = pm.run(circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)
isa_circuit.draw("mpl", fold=-1)

Output of the previous code cell

Закручування (twirl) шарів двокубітних гейтів та вимірювань, і пошук унікальних шарів​

Ми використовуємо samplomatic, щоб боксувати схему та ідентифікувати унікальні двокубітні шари. Бокс — це конструкція, яка групує інструкції разом, щоб конкретні наміри або анотації можна було пізніше застосувати уніфіковано до всіх гейтів у межах одного боксу.

Тут ми викликаємо метод generate_boxing_pass_manager, який виходить за межі простої ідентифікації двокубітних шарів. Він виконує кілька ключових завдань:

  • Групує всі двокубітні шари в схемі,

  • Застосовує анотації Twirl та ChangeBasis до цих шарів,

  • Групує операції вимірювання у власні боксовані секції, і

  • Застосовує анотацію InjectNoise до кожного двокубітного шару.

Ці анотації визначають, як шум, зміни базису та закручування обробляються по всій схемі. Вони також встановлюють структуру, яка використовується пізніше для вивчення та пом'якшення шуму.

Ключові параметри конфігурації:

  • enable_gates/enable_measures: True: Боксувати всі шари двокубітних гейтів і термінальні вимірювання. Однокубітні гейти залишаються "одягненими" зліва в межах боксів.

  • measure_annotations: all: Включити анотації Twirl та ChangeBasis до боксу вимірювання.

  • twirling_strategy: active: Закручувати всі активні кубіти в кожному боксі, що містить заплутуючі гейти.

  • inject_noise_targets: gates: Додати анотації InjectNoise до всіх боксів з анотацією Twirl, що містять заплутуючі гейти.

  • inject_noise_strategy: uniform_modification: Масштабувати всі шари шуму еквівалентно по всій схемі.

# Box up circuit with Twirl and InjectNoise annotations
pm = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
measure_annotations="all",
twirling_strategy="active",
inject_noise_targets="gates",
inject_noise_strategy="uniform_modification",
)
boxed_circuit = pm.run(isa_circuit)
draw_circ = QuantumCircuit(boxed_circuit.num_qubits)
draw_circ.append(boxed_circuit.data[0], qargs=boxed_circuit.data[0].qubits)
draw_circ.append(boxed_circuit.data[1], qargs=boxed_circuit.data[1].qubits)
draw_circ.draw("mpl", fold=-1, scale=0.3, idle_wires=False)

Output of the previous code cell

Згенерувати шаблонну схему та samplex, які визначають, як буде семплюватися схема.

Тут ми також додаємо вимірювання спостерігача (spectator) та пост-селекції, які потрібні для виконання пост-селекції на семплах, отриманих з Executor.

# Build template circuit and samplex for later use with the "Executor"
template_circuit, samplex = samplomatic.build(boxed_circuit)

# Add post-selection instructions to the template circuit
post_selection_pm = PassManager(
[
AddSpectatorMeasures(backend.coupling_map),
AddPostSelectionMeasures(x_pulse_type="rx"),
]
)
template_circuit = post_selection_pm.run(template_circuit)
draw_circ = template_circuit.copy_empty_like()
draw_circ.data = template_circuit.data[:324]
draw_circ.draw("mpl", fold=-1, scale=0.3, idle_wires=False)

Output of the previous code cell

Вивчення шуму за допомогою NoiseLearnerV3​

Перш ніж ми зможемо застосувати PNA для пом'якшення помилок, нам спочатку потрібно характеризувати шум, що впливає на кожен унікальний двокубітний шар та шар вимірювання в нашій схемі. Для цього ми використовуємо програму NoiseLearnerV3, щоб експериментально вивчити моделі шуму для кожного шару, ідентифікованого раніше. Програма вивчення шуму виконує експерименти у стилі бенчмаркінгу, які оцінюють канал шуму, що впливає на кожен шар, і повертає об'єкт результату, що містить вивчену модель.

Ми починаємо з ідентифікації унікальних шарів у нашій схемі за допомогою find_unique_box_instructions з samplomatic. Це гарантує, що ми вивчаємо шум лише один раз для кожного окремого типу шару, мінімізуючи кількість експериментів та загальну вартість запусків (shots). Отриманий список шарів передається в програму вивчення шуму.

Є кілька ключових параметрів, які контролюють, як вивчається шум:

  • num_randomizations: Кількість випадкових схем, що використовуються для кожної конфігурації навчання.

  • shots_per_randomization: Кількість запусків (shots), зроблених на кожну випадкову навчальну схему.

  • layer_pair_depths: Глибини схеми (виміряні в кількості пар), що використовуються в навчальних експериментах.

  • post_selection: Вмикає пост-селекцію на основі ребер, використовуючи гейти rx для застосування пульсів після вимірювання.

# Noise learning parameters
num_randomizations_nl = 64
shots_per_randomization_nl = 128

# Match the deepest noise-learning depth to the depth of the circuit being
# mitigated. ``layer_pair_depths`` are measured in layer pairs (a layer + its
# inverse), so the deepest value is half the circuits's two-qubit-layer depth.
# Learning to this depth improves the quality of the mitigation.

depth_2q = isa_circuit.depth(lambda instr: instr.operation.num_qubits == 2)
max_layer_pair_depth = depth_2q // 2

# Use a fixed schedule of learning depths, but drop any that exceed the circuit's
# depth and always cap the deepest value at ``max_layer_pair_depth`` so we never
# learn deeper than the circuit being mitigated
candidate_depths = [1, 2, 4, 8, 12, 16, 24, 32, 40, 48]
layer_pair_depths = sorted(
{d for d in candidate_depths if d < max_layer_pair_depth}
| {max_layer_pair_depth}
)

# Find the unique instructions (layers) from the boxed-up circuit
unique_2q_layers_and_meas = find_unique_box_instructions(
boxed_circuit, normalize_annotations=None, undress_boxes=True
)

# Configure and run the noise learner on the unique layers.
# Options can be passed directly as a dictionary.
noise_learner_options = {
"num_randomizations": num_randomizations_nl,
"shots_per_randomization": shots_per_randomization_nl,
"layer_pair_depths": layer_pair_depths,
"post_selection": {
"enable": True,
"strategy": "edge",
"x_pulse_type": "rx",
},
"environment": {"job_tags": ["TUT_PNA"]},
}

noise_learner = NoiseLearnerV3(backend, noise_learner_options)
noise_learner_job = noise_learner.run(unique_2q_layers_and_meas)
noise_learner_result = noise_learner_job.result()

Візуалізація вивчених показників шуму​

Після вивчення моделей шуму ми можемо перевірити розподіл виведених показників помилок як для однокубітних, так і для двокубітних операцій. Наведений нижче код витягує представлення Паулі-Ліндблада з результатів вивченого шуму та збирає відповідні показники шуму.

Для кожного вивченого шару:

  • Ми перетворюємо модель шуму в розріджений список кортежів (pstr, qubits, rate), де pstr — це рядок Паулі, що діє на задані кубіти, а rate — відповідний показник помилки.
  • Ми розділяємо показники на однокубітні (len(pstr) == 1) та двокубітні (len(pstr) == 2) доданки.
  • Потім списки показників сортуються, і обчислюються їхні медіанні значення.

Ми будуємо графіки розподілів показників шуму для одного кубіта (червоний) та двох кубітів (синій) у логарифмічному масштабі, з їхніми медіанними значеннями, позначеними вертикальними лініями, щоб ми могли порівняти відносні величини вивчених генераторів Паулі-Ліндблада. Порядок показників для одного та двох кубітів залежить від пристрою та конкретних шарів, що характеризуються; у цьому запуску однокубітні (вага-1) генератори мають більший медіанний показник.

hw_rates_1q = []
hw_rates_2q = []
for nlr in noise_learner_result[:2]:
plm_list = nlr.to_pauli_lindblad_map().to_sparse_list()
hw_rates_1q += [
rate for (pstr, qubits, rate) in plm_list if len(pstr) == 1
]
hw_rates_2q += [
rate for (pstr, qubits, rate) in plm_list if len(pstr) == 2
]
hw_rates_1q = sorted(hw_rates_1q)
hw_rates_2q = sorted(hw_rates_2q)
median_1q = hw_rates_1q[len(hw_rates_1q) // 2]
median_2q = hw_rates_2q[len(hw_rates_2q) // 2]
fig, ax = plt.subplots(1, 1, figsize=(14, 5))
ax.scatter(
(hw_rates_1q),
[(i) / (len(hw_rates_1q) - 1) for i in range(len(hw_rates_1q))],
color="red",
label="1q rates",
)
ax.set_xscale("log")
ax.set_ylim(0, 1.1)
ax.vlines(median_1q, 0, 1, color="red")
ax.text(median_1q * 1.1, 0.1, f"{median_1q:.2e}")
ax.scatter(
(hw_rates_2q),
[(i) / (len(hw_rates_2q) - 1) for i in range(len(hw_rates_2q))],
color="blue",
label="2q rates",
)
ax.set_xscale("log")
ax.set_ylim(0, 1.1)
ax.vlines(median_2q, 0, 1, color="blue")
ax.text(median_2q * 1.1, 0.2, f"{median_2q:.2e}")
ax.set_title("Learned noise rates")
ax.set_xlabel("Noise rate")
ax.set_yticks([])
plt.legend()
<matplotlib.legend.Legend at 0x125336120>

Output of the previous code cell

Асоціювання боксів схеми з вивченим шумом​

Після того, як ми отримали моделі шуму для кожного унікального двокубітного шару, нам потрібно пов'язати їх з відповідними анотаціями InjectNoise у боксованій схемі.

Директива InjectNoise — це анотація samplomatic, яка використовує однокубітні одягачі (dressers), щоб контрольовано та конфігуровано вводити шум у схему. Вона дозволяє модульне моделювання шуму на різних шарах.

Кожна анотація InjectNoise включає:

  • InjectNoise.ref - унікальний ідентифікатор анотації. Він використовується об'єктом samplex для правильного присвоєння відповідної моделі шуму.
  • InjectNoise.modifier_ref (необов'язково) - вторинне посилання, яке дозволяє масштабувати призначену модель шуму на мультиплікативний коефіцієнт.

На цьому кроці ми будуємо відображення від кожного InjectNoise.ref до відповідної вивченої моделі шуму (PauliLindbladMap). Це відображення гарантує, що кожен шар заплутуючих гейтів у схемі поєднується з відповідною моделлю шуму, тож ефекти шуму точно застосовуються під час семплювання та подальших кроків пом'якшення шуму.

# map inject noise refs to pauli lindblad maps
refs_to_noise_models = {}
for instruction, result in zip(
unique_2q_layers_and_meas, noise_learner_result, strict=False
):
if inject_noise_annot := get_annotation(
instruction.operation, InjectNoise
):
refs_to_noise_models[inject_noise_annot.ref] = (
result.to_pauli_lindblad_map()
)

Поширення спостережуваної величини через вивчений анти-шум​

Як обговорювалося вище, це робиться у два кроки. Спочатку ми поширюємо генератор анти-шуму до кінця схеми. Після цього ми поширюємо спостережувану величину через цей еволюціонований генератор. Цей процес повторюється для кожного генератора анти-шуму в схемі. У цій реалізації кожен генератор у заданому шарі поширюється до кінця схеми паралельно. Крім того, багатопроцесорність Python використовується для паралельного виконання як прямого поширення анти-шуму, так і зворотного поширення спостережуваної величини. Це запобігає накопиченню еволюціонованих генераторів у пам'яті, а також максимізує обчислювальні ресурси.

Під час запуску PNA тобі завжди потрібно надати шумну схему та спостережувану величину. Якщо твоя шумна схема — це боксована схема з анотаціями InjectNoise, тобі потрібно надати відображення, яке ми створили на кроці вище. Можна також передати не боксовану схему, що містить інструкції PauliLindbladError з qiskit-aer. У цьому разі refs_to_noise_models не обов'язково надавати. Окрім основних вхідних даних, розглянь також наступне:

  • max_err_terms: Кількість доданків, які потрібно зберегти в кожному генераторі анти-шуму під час прямого поширення. Більше значення зазвичай підвищує точність, але монотонність цього зростання не гарантована.

  • max_obs_terms: Кількість доданків, які потрібно зберегти в спостережуваній величині, що пом'якшує шум, O~\tilde{O}, під час зворотного поширення через еволюціонований анти-шум. Більші значення зазвичай підвищують точність, але це не гарантовано монотонно.

  • num_processes: Кількість ядер, які виділяються для процесу. Пам'ятай, що генератори поширюються вперед і застосовуються до спостережуваної величини паралельно.

  • search_step: Крок зворотного поширення використовує жадібний метод для приблизного спряження двох операторів у базисі Паулі. Цей метод можна прискорити, збільшивши search_step. Дивись документацію pauli-prop для отримання додаткової інформації.

  • num_to_measure: Хоча ця змінна не є вхідним параметром для generate_noise_mitigating_observable, ми використовуємо її, щоб контролювати, скільки доданків з O~\tilde{O} ми насправді хочемо виміряти. Тут ми вимірюємо лише 30 найбільших доданків, які є початковими доданками нашої спостережуваної величини. Ці доданки тепер перемасштабовані так, що їхнє вимірювання має ефект пом'якшення вивченого шуму гейтів. Хоча ми вимірюємо лише 30 доданків з O~\tilde{O}, часто все ще корисно дозволити йому зростати, оскільки це підвищує точність коефіцієнтів масштабування провідних доданків.

# PNA parameters
num_processes = 8
max_err_terms = 10_000
max_obs_terms = 10_000
num_to_measure = num_qubits

obs_tilde_isa = generate_noise_mitigating_observable(
boxed_circuit,
isa_observable,
refs_to_noise_models,
max_err_terms=max_err_terms,
max_obs_terms=max_obs_terms,
num_processes=num_processes,
print_progress=True,
search_step=8,
)
p_2_v = {p: v for v, p in enumerate(layout)}
obs_tilde_virtual = SparsePauliOp.from_sparse_list(
[
(pstr, [p_2_v[p] for p in p_qubits], coeff)
for (pstr, p_qubits, coeff) in obs_tilde_isa.to_sparse_list()
],
num_qubits=num_qubits,
)
obs_tilde_virtual = obs_tilde_virtual[
np.argsort(np.abs(obs_tilde_virtual.coeffs))[::-1]
][:num_to_measure]
Finished! 13740 / 13740 generators propagated.
obs_tilde_isa = obs_tilde_isa[np.argsort(np.abs(obs_tilde_isa.coeffs))][::-1]
plt.xscale("log")
plt.yscale("log")
plt.title(r"$\tilde{O}$ coeff magnitudes")
plt.ylabel("Magnitude")
plt.xlabel("Pauli term index")
plt.plot(np.abs(obs_tilde_isa.coeffs), ".")
[<matplotlib.lines.Line2D at 0x124b0a120>]

Output of the previous code cell

Перетворення базисів вимірювання в канонічну форму​

Далі ми знаходимо мінімальний набір базисів для вимірювання таким чином, щоб повністю покрити кожен доданок Паулі у вимірюваній спостережуваній величині (багато спостережуваних величин можна вимірювати одночасно, якщо вони комутують покубітно). Оскільки ми вимірюємо лише доданки в нашій оригінальній спостережуваній величині, яка є сумою всіх однокубітних Z-Паулі, потрібен лише один базис: базис усіх-Z.

Окрім знаходження набору базисів вимірювання Паулі, нам потрібно звести ці доданки Паулі до канонічної форми, якої очікує Executor. Для отримання додаткової інформації про канонічне впорядкування кубітів відвідай документацію Samplomatic.

meas_box = boxed_circuit.data[-1]
canonical_qubits = [
idx
for idx, qubit in enumerate(boxed_circuit.qubits)
if qubit in meas_box.qubits
]
c_2_p = {
c: p for c, p in enumerate(canonical_qubits)
} # canonical -> physical
p_2_v = {p: v for v, p in enumerate(layout)} # physical -> virtual
c_2_v = {c: p_2_v[p] for c, p in c_2_p.items()} # canonical -> virtual
meas_bases, bases_reverser = get_measurement_bases(obs_tilde_virtual)
meas_bases_canonical = [
np.array([base[c_2_v[c]] for c in range(num_qubits)], dtype=np.uint8)
for base in meas_bases
]

Крок 3: Виконання квантових експериментів​

Визначення способу семплювання в QuantumProgram​

Тепер ми налаштовуємо QuantumProgram, який слугує центральним контейнером для всіх схем та конфігурацій семплювання, що будуть виконуватися Executor. Цей об'єкт визначає, як генеруються, групуються та виконуються рандомізовані екземпляри схем для отримання результатів вимірювання, що використовуються в PNA.

QuantumProgram може містити кілька елементів, кожен з яких складається з шаблонної схеми та відповідного об'єкта samplex, що визначає, як застосовуються рандомізації. Ця абстракція дозволяє Executor обробляти весь робочий процес як єдину, модульну програму — від генерації рандомізованих схем до збору та агрегації запусків (shots).

На цьому кроці ми створюємо QuantumProgram, який запускає наш експеримент PNA, використовуючи шаблонну схему та samplex, які ми побудували раніше. Конфігурація включає такі елементи:

  • template_circuit: Схема, що містить усі гейти, необхідні для реалізації всіх бажаних рандомізацій (з рандомізацій закручування, параметрів тощо).

  • samplex: Об'єкт, що визначає розподіл ймовірностей за всіма можливими рандомізаціями схеми, з яких здійснюється семплювання.

  • samplex_arguments: Прив'язки, необхідні для повного визначення samplex

    • basis_changes: Тут ми вказуємо набір базисів для вимірювання, які покриють усі доданки Паулі у вимірюваній спостережуваній величині.

    • noise_scales.ref: Ми встановлюємо масштаб кожного шару шуму на 0.0, щоб запобігти введенню будь-якого додаткового шуму в наші семпли.

    • pauli_lindblad_maps: Обов'язково, якщо передано noise_scales. Це просто відображає шари шуму на відповідну модель шуму.

  • shape: Кортеж форми для розширення неявної форми, визначеної samplex_arguments. Нетривіальні осі, введені цим розширенням, перелічують рандомізації.

# Control the # of shots during execution
shots_per_randomization_exec = 64
num_randomizations_exec = 6144

# Zero out the noise to prevent noise from being injected during execution.
# We only added InjectNoise annotations so PNA could associate the noise
# to layers in the circuit
samplex_inputs = {f"noise_scales.{ref}": 0.0 for ref in refs_to_noise_models}
samplex_inputs |= {"pauli_lindblad_maps": refs_to_noise_models}

# Specify the bases to measure. The samplex exposes one basis-change input per
# ChangeBasis-annotated box; here a single all-Z basis covers every term. We
# look up the basis-change interface name rather than hardcoding an index, since
# the name depends on the circuit's box structure.
bases_broadcastable = np.expand_dims(np.array(meas_bases_canonical), axis=1)
samplex_inputs |= {
spec.name: bases_broadcastable
for spec in samplex.inputs().get_specs(r"^basis_changes\.")
}

# Convert samplex_inputs into a dict to pass to QuantumProgram
samplex_arguments = (
samplex.inputs().make_broadcastable().bind(**samplex_inputs)
)

# Instantiate the QuantumProgram with the specified parameters
program = QuantumProgram(shots=shots_per_randomization_exec)
program.append_samplex_item(
circuit=template_circuit,
samplex=samplex,
samplex_arguments=samplex_arguments,
shape=(num_randomizations_exec,),
)

Семплювання схеми за допомогою Executor​

Тепер, коли ми визначили нашу QuantumProgram, виконання експерименту є простим. Ми просто створюємо екземпляр об'єкта Executor, надаємо йому бекенд і запускаємо програму.

# Execute (sample) the circuit
executor = Executor(backend)
job_exec = executor.run(program)
exec_results = job_exec.result()

Крок 4: Реконструкція та аналіз результатів​

Щоб обчислити очікуване значення з пом'якшеними помилками, ми виконуємо таке:

  • Обчислюємо коефіцієнти масштабування TREX на основі вивченого шуму, що впливає на вимірювання,

  • Генеруємо маску для збереження лише семплів, що пройшли пост-селекцію, і

  • Використовуємо функцію executor_expectation_values з qiskit-addon-utils для об'єднання всіх даних в очікуване значення з пом'якшеними помилками.

# Computing the TREX factors
measurement_noise_map = noise_learner_result[2].to_pauli_lindblad_map()
trex_rescale_factors = trex_factors(measurement_noise_map, bases_reverser)

# Post-select the results
post_selector = PostSelector.from_circuit(
circuit=template_circuit, coupling_map=backend.coupling_map
)

# Compute the ps mask for filtering results
mask = post_selector.compute_mask(exec_results[0], strategy="edge")

# Compute expvals using post selected results
results = executor_expectation_values(
exec_results[0]["meas"],
bases_reverser,
meas_basis_axis=0,
avg_axis=1,
measurement_flips=exec_results[0]["measurement_flips.meas"],
pauli_signs=exec_results[0].get("pauli_signs", None),
postselect_mask=mask,
rescale_factors=trex_rescale_factors,
)

Порівняння стратегій пом'якшення: PNA, PNA+TREX, PNA+PS, PNA+PS+TREX​

Ми обчислюємо та візуалізуємо очікувані значення для кількох варіантів пом'якшення, використовуючи результати Executor.

bases_reverser_unmit = {Pauli("Z" * num_qubits): [observable]}
args = [
(bases_reverser_unmit, None, None),
(bases_reverser, None, None),
(bases_reverser, None, trex_rescale_factors),
(bases_reverser, mask, None),
(bases_reverser, mask, trex_rescale_factors),
]

evs = []
for reverser, postsel_mask, factors in args:
# Compute expvals using post selected results
res_ps = executor_expectation_values(
exec_results[0]["meas"],
reverser,
meas_basis_axis=0,
avg_axis=1,
measurement_flips=exec_results[0]["measurement_flips.meas"],
pauli_signs=exec_results[0].get("pauli_signs", None),
postselect_mask=postsel_mask,
rescale_factors=factors,
)
res_ps = np.array(res_ps)
evs.append(res_ps[:, 0][0])

experiments = ["PNA", "PNA+TREX", "PNA+PS", "PNA+PS+TREX"]
colors = ["#d9d9d9", "#b0b0b0", "#7f7f7f", "#4c4c4c"]
plt.bar(experiments, evs[1:], color=colors)
plt.axhline(y=1, color="green", linestyle="--", linewidth=2, label="Ideal")
plt.axhline(
y=evs[0], color="red", linestyle="--", linewidth=2, label="Unmitigated"
)
plt.ylabel("Expectation value", fontsize=14)

plt.title(
r"30q Mirrored Ising, 10 Trotter steps, $\theta_{rx}=\frac{\pi}{8}$",
fontsize=14,
)
plt.legend(loc="upper left", bbox_to_anchor=(1.05, 1), borderaxespad=0.0)
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

Output of the previous code cell

Результати демонструють кумулятивні переваги поєднання різних технік пом'якшення помилок. Простий підхід PNA вже відновлює очікуване значення близько до ідеального еталона, що вказує на те, що поширення обернених каналів шуму в спостережувану величину ефективно компенсує помилки двокубітних гейтів.

  • Додавання переважування TREX (PNA+TREX) дещо покращує оцінку, коригуючи дисбаланс семплювання в рандомізованих схемах.

  • Пост-селекція (PNA+PS) забезпечує помітніше покращення, відфільтровуючи непослідовні результати вимірювання, які, ймовірно, є наслідком залишкових помилок.

  • Нарешті, поєднання обох (PNA+PS+TREX) дає найточніший результат, майже відповідний ідеальному значенню, показуючи, як ці стратегії пом'якшення підсилюють одна одну.

Загалом, порівняння підкреслює, що PNA слугує надійною основою для оцінки очікуваних значень з урахуванням шуму, тоді як TREX та пост-селекція пропонують взаємодоповнюючі вдосконалення для подальшого підвищення точності.

Наступні кроки​

Якщо ця робота видалася тобі цікавою, тебе можуть зацікавити такі матеріали: