Перейти до основного вмісту

Ймовірнісне скасування похибок із затіненими світловими конусами

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

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

  • Що таке ймовірнісне скасування похибок (PEC) і чому його накладні витрати на вибірку γ2\gamma^2 зростають експоненційно із загальним шумом, що діє на коло

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

  • Як вивчати шум шарів за допомогою NoiseLearnerV3 та вводити антишум через samplomatic і примітив Executor

  • Як поєднати PEC та PEC+SLC з TREX і постселекцією для оцінки очікуваного значення на апаратному забезпеченні

Prerequisites

Background

Цей підручник демонструє, як пом'якшувати похибки за допомогою додону затінених світлових конусів (SLC). Цей додон є розвитком техніки ймовірнісного скасування похибок (PEC), у якій користувач вивчає шум унікальних шарів кола, а потім скасовує шум, застосовуючи однокубітні вентилі та методи постобробки. Порівняно з іншими методами, PEC пропонує більш надійні межі зміщення пом'якшеного результату, але має тенденцію страждати від вищих накладних витрат у термінах часу QPU. Під час PEC, щоб компенсувати послаблення очікуваного значення шумом, середній результат перемасштабується на фактор γ=exp(l,σ2λl,σ)\gamma = \exp(\sum_{l,\sigma} 2\lambda_{l,\sigma}), де λl,σ\lambda_{l,\sigma} — вивчена швидкість шуму помилки Паулі σ\sigma на шарі ll у колі. Це перемасштабування збільшує дисперсію на фактор γ2\gamma^2 і, отже, також множить кількість виконань кола, необхідних на QPU, на γ2\gamma^2, що ми називаємо витратами на вибірку або накладними витратами на вибірку. Оскільки γ\gamma зростає експоненційно, PEC часто обмежується дрібними колами або колами з малою кількістю кубітів. Дізнайся більше про PEC у Ймовірнісне скасування похибок з розрідженими моделями Паулі-Ліндблада на зашумлених квантових процесорах.

Якщо ми можемо визначити похибки, які не потребують пом'якшення, ми можемо зменшити ці витрати на вибірку експоненційно. Першим кроком у цьому напрямку є впровадження локально-обізнаного пом'якшення похибок, яке використовує швидко обчислюваний звичайний «світловий конус» для зменшення накладних витрат PEC шляхом обмеження чутливості спостережуваної величини до похибок протягом усього кола, розширюючи придатність PEC до більших масштабів для деяких задач. Похибки поза цим світловим конусом не можуть впливати на виміряний результат і тому можуть бути виключені з процедури скасування похибок. Це виключення зменшує накладні витрати на вибірку, у деяких випадках суттєво, не вносячи додаткового зміщення. Зокрема, для вимірювання локальної спостережуваної величини OO кола фіксованої глибини необхідні накладні витрати на вибірку зрештою виходять на плато при масштабуванні кількості кубітів у колі (див. Рис. 2b у Локальність та пом'якшення похибок квантових кіл).

Затінені світлові конуси (SLC) йдуть далі, використовуючи класичні симуляції для точнішого обмеження чутливості до похибок протягом усього кола. Це обмінює частину часу QPU на час CPU і зменшує накладні витрати на вибірку, необхідні для перенормування зміщення. Замість жорсткого відсічення, кожній потенційній похибці в колі присвоюється градуйований «відтінок», що обмежує зверху чутливість очікуваного значення спостережуваної величини до цієї похибки. (Для введених нижче меж зворотного поширення ця чутливість також залежить від початкового стану кола — усе-нульового стану в типових робочих процесах і в цьому підручнику.) Це уточнене характеризування дозволяє більш ефективні, цілеспрямовані застосування PEC зі зменшеною дисперсією, водночас надаючи користувачу можливість контрольовано налаштовувати зміщення в оцінюванні спостережуваної величини. Дивись Затінення світлових конусів для класично прискореного пом'якшення квантових похибок для більш детальної інформації.

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

SLC error-mitigation workflow at a glance

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

a. CPU — Обмеження впливу окремих похибок одно- та двокубітних похибок Паулі

  1. Пряме поширення (обмеження впливу на спостережувану величину). Поширюємо кожну похибку до кінця кола і обчислюємо її комутатор зі спостережуваною величиною.

    • Усічення членів оператора під час еволюції, щоб обчислення залишалося посильним.

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

  2. Зворотне поширення (обмеження впливу на початковий стан). Поширюємо кожну похибку до початку кола і обчислюємо її комутатор з початковим станом.

b. QPU — Вивчення швидкостей шуму. Використовуй NoiseLearnerV3, щоб оцінити швидкості моделі шуму Паулі-Ліндблада.

c. CPU — Пріоритезація пом'якшення

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

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

d. QPU — Введення антишуму та запуск. Виконуємо цільове коло з антишумом (оберненим шумом), заданим за допомогою анотацій Box.

e. CPU — Оцінка спостережуваної величини. Обчислюємо очікуване значення, застосовуючи постселекцію на основі вимірювань для зменшення впливу немарковського шуму.

Огляд навчання шуму

Вивчення шуму — це звичайний крок у кількох методах пом'якшення похибок, здійснюваний вивчником шуму; він також з'являється в підручнику про ймовірнісне посилення похибок. У NoiseLearnerV3 ти можеш конкретно вказати шари шуму, які потрібно вивчити, як об'єкти CircuitInstruction, щоб можна було обчислити бажані межі шуму SLC для кожного шару у спосіб, описаний вище. Вивчена модель Паулі-Ліндблада надає коефіцієнти, які використовуються в пріоритезації PEC+SLC. Спосіб, у який вентилі збираються в шари, можна визначити за допомогою зручних функцій generate_boxing_pass_manager та find_unique_box_instructions, а потім передати в утилітну функцію SLC generate_noise_model_paulis, як описано на Кроці 2 нижче.

Частина 1Частина 2Частина 3
Твірлінг Паулі шарів двокубітних вентилівПовторення пар шарів тотожності та вивчення шумуВиведення точності (похибка для кожного каналу шуму)
Pauli twirlingLearn layerCurve fit

Огляд постобробки

Після виконання на квантовому апаратному забезпеченні за допомогою фреймворку samplomatic та Executor, ми перетворюємо наші вимірювання бітових рядків у бажане значення спостережуваної величини. У цьому підручнику ми демонструємо робочий процес на дзеркальному колі Ізінга (побудованому на Кроці 1 нижче), для якого ідеальне очікуване значення виміряної спостережуваної величини дорівнює 1, оскільки друга половина кола скасовує першу. При обчисленні значення спостережуваної величини за допомогою функції executor_expectation_values ми застосовуємо кілька методів постобробки, що зменшують вплив шуму. Вони включають видалення пострілів, на які вплинув немарковський шум, пом'якшення похибок зчитування та врахування деталей нашої реалізації PEC. Деталі обговорюються на Кроці 4 нижче.

Requirements

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

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

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

  • Qiskit addon затінених світлових конусів v0.1 або новішу версію (pip install qiskit-addon-slc)

  • Qiskit addon utils v0.3 або новішу версію (pip install qiskit-addon-utils)

  • Samplomatic v0.13 або новішу версію (pip install samplomatic)

Setup

Спочатку імпортуй пакети та функції, необхідні для запуску цього ноутбука.

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-slc qiskit-addon-utils qiskit-ibm-runtime samplomatic
from multiprocessing import set_start_method

# Setting this value prevents itertools.starmap deadlock on UNIX systems
set_start_method("spawn")

# Needed to prevent PySCF from parallelizing internally (SLC only)
%set_env OMP_NUM_THREADS=1
env: OMP_NUM_THREADS=1
import numpy as np
from matplotlib import pyplot as plt

from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager, PassManager

from qiskit_ibm_runtime import (
QiskitRuntimeService,
QuantumProgram,
Executor,
NoiseLearnerV3,
)

import samplomatic
from samplomatic.utils import find_unique_box_instructions
from samplomatic.transpiler import generate_boxing_pass_manager

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 (
gamma_from_noisy_boxes,
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_slc.bounds import (
compute_backward_bounds,
compute_forward_bounds,
compute_local_scales,
merge_bounds,
tighten_with_speed_limit,
)
from qiskit_addon_slc.utils import (
generate_noise_model_paulis,
map_modifier_ref_to_ref,
)
from qiskit_addon_slc.visualization import draw_shaded_lightcone

Приклад малого симулятора

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

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

  • Примітив Executor вибирає завірлені кола з введеним антишумом, згенеровані samplomatic, на бекенді.

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

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

Ми запускаємо повний робочий процес PEC+SLC на дзеркальному колі Ізінга з 20 кубітами, виконуваному на апаратному забезпеченні IBM Quantum®, дотримуючись чотирьох кроків шаблону Qiskit.

Крок 1: Відобразити задачу

Наша демонстрація налаштована так:

  1. Ми обираємо виконати еволюцію в часі під дією гамільтоніана 1D ланцюга Ізінга. 1D ланцюг Ізінга дає приємно щільну структуру кола, що зручно для демонстрації реалізацій PEC.

  2. Ми обираємо вимірювати спостережувану величину X6Z13\langle X_6 Z_{13}\rangle, яка містить одну компоненту XX та одну компоненту ZZ. Це зручно для висвітлення того, як межі SLC по-різному обробляють різні типи похибок.

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

  4. Нарешті, отримання ідеального очікуваного значення 1 вимагає, щоб схема починалася — і, отже, завдяки дзеркальному відображенню, ідеально також закінчувалася — у стані з власним значенням +1 спостережуваного. Оскільки спостережуване містить компоненту, відмінну від ZZ, а примітив Executor враховує бажаний базис вимірювання наприкінці схеми, ми додаємо відповідні гейти підготовки базису на початку дзеркальної схеми за допомогою функції prepare_basis. Ми використовуємо функцію get_measurement_bases, щоб визначити, які гейти потрібні та де їх розмістити, водночас відстежуючи тонкощі індексації кубітів, що виникають через угоди анотацій box, як обговорюється в розділі про підготовку канонічних вимірювань базису.

# Width of the Ising chain (a free parameter of this demonstration)
num_qubits = 20

# The observable to measure: one X component (on qubit 6) and one Z component
# (on qubit 13), as motivated in "Step 1: Map the problem" above
target_obs_sparse = [("XZ", [6, 13], 1.0)]
observable = SparsePauliOp.from_sparse_list(
target_obs_sparse, num_qubits=num_qubits
)
# Determine the measurement basis each qubit requires for this observable.
# The reverser is used during post-processing in Step 4.
bases_virt, reverser_virt = get_measurement_bases(observable)
# Free parameters of the demonstration circuit: the number of Trotter steps
# sets the circuit depth, and rx_angle is the transverse-field rotation angle
# applied in each step (pi/4 makes the dynamics non-Clifford)
num_trotter_steps = 10
rx_angle = np.pi / 4
def construct_ising_circuit(
num_qubits: int,
num_trotter_steps: int,
rx_angle: float,
) -> QuantumCircuit:
circuit = QuantumCircuit(num_qubits)

for _ 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)

return circuit

def prepare_basis(
circuit: QuantumCircuit, basis: list[int]
) -> QuantumCircuit:
# basis is a list of integer values from 0 to 3. These map to the basis measurement as:
# 0 = I; 1 = Z; 2 = X; 3 = Y
assert len(basis) == circuit.num_qubits

out_circ = circuit.copy_empty_like()
for qb, bas in enumerate(basis):
if bas == 2:
out_circ.h(qb)
elif bas == 3:
out_circ.rx(-np.pi / 2, qb)

out_circ.barrier()
out_circ.compose(circuit, inplace=True)
return out_circ

def mirror_circuit(circuit: QuantumCircuit) -> QuantumCircuit:
mirror_circ = circuit.copy_empty_like()
mirror_circ.compose(circuit.inverse(), inplace=True)
mirror_circ.barrier()
mirror_circ.compose(circuit, inplace=True)
mirror_circ.measure_active()
return mirror_circ
# Instantiate the mirrored circuit and prepend the basis-preparation gates
circuit = construct_ising_circuit(num_qubits, num_trotter_steps, rx_angle)
mirrored_circuit = mirror_circuit(circuit)
mirrored_circuit = prepare_basis(mirrored_circuit, bases_virt[0])
mirrored_circuit.draw(
"mpl", fold=-1, scale=0.3, idle_wires=False, measure_arrows=False
)

Output of the previous code cell

Крок 2: Оптимізація

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

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

# Select the least busy Heron device. This tutorial targets Heron processors:
# the usage estimate and the non-Markovian-noise post-selection discussed in
# Step 4 are specific to this processor family.
backend = service.least_busy(
operational=True,
simulator=False,
filters=lambda b: b.processor_type["family"] == "Heron",
)
# 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_kingston

Спочатку ми транспілюємо нашу схему в інструкції ISA, що потрібно для виконання на наших QPU. Вибір фізичних кубітів для запуску значно впливає на якість результатів, оскільки часи когерентності та точність гейтів варіюються по пристрою і дрейфують з часом. Для ретельного експерименту може окупитися вибір ланцюжка кубітів вручну на основі швидких експериментів з бенчмаркінгу (див., наприклад, посібник Бенчмаркінг у реальному часі для вибору кубітів). Для цілей цього посібника ми покладаємося на транспілятор Qiskit для вибору розташування на основі карти зв'язків бекенда та найновіших даних калібрування.

isa_pm = generate_preset_pass_manager(backend=backend, optimization_level=1)

isa_circuit = isa_pm.run(mirrored_circuit)

# The chain of physical qubits selected by the transpiler
layout = isa_circuit.layout.final_index_layout()
print(f"Selected layout: {layout}")

isa_observable = observable.apply_layout(
layout, num_qubits=isa_circuit.num_qubits
)
Selected layout: [71, 58, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 36, 21, 22, 23, 16, 3, 2]
wire_order = layout + [
q for q in range(isa_circuit.num_qubits) if q not in layout
]
isa_circuit.draw(
"mpl",
fold=-1,
scale=0.3,
idle_wires=False,
wire_order=wire_order,
measure_arrows=False,
)

Output of the previous code cell

Розміщення схеми в блоках

Для простоти реалізації ми використовуємо прохід транспіляції generate_boxing_pass_manager, який розміщує інструкції схеми в анотовані блоки. Ці блоки чітко вказують, де, у випадку PEC, антишум має бути введений у схему. Деталі щодо налаштувань дивіться в документації Samplomatic.

Зауважте, що робочий процес SLC вимагає використання inject_noise_strategy="individual_modification" пізніше в процесі, оскільки це дозволяє нам однозначно ідентифікувати анотації InjectNoise інструкцій BoxOp у схемі. (Блоки без анотації InjectNoise не можуть бути ідентифіковані таким чином.)

обережно

Обчислення меж SLC наразі передбачають, що анотації InjectNoise розміщені з site="after" (див. qiskit-addon-slc#33). Оскільки значення за замовчуванням цього параметра змінилося в останніх версіях samplomatic (див. samplomatic#372), ми явно встановлюємо inject_noise_site="after" нижче.

Функція find_unique_box_instructions проходить через надану схему з блоками та визначає ті, що мають унікальні двокубітні (2Q) шари або вимірювання, для цілей навчання шуму та введення шуму.

# Box circuit with Twirl and InjectNoise annotations
boxes_pm = generate_boxing_pass_manager(
twirling_strategy="active",
inject_noise_strategy="individual_modification",
inject_noise_site="after",
inject_noise_targets="gates",
measure_annotations="all",
)

boxed_circuit = boxes_pm.run(isa_circuit)

# Find the unique instructions (layers) from boxed circuit
unique_2q_instructions = find_unique_box_instructions(
boxed_circuit, normalize_annotations=None, undress_boxes=True
)
boxed_circuit.draw(
"mpl",
fold=-1,
scale=0.3,
idle_wires=False,
wire_order=wire_order,
measure_arrows=False,
)

Output of the previous code cell

Підготовка вимірювань канонічних базисів

Зауважте, що ми повинні особливо ретельно відстежувати порядок кубітів. Нижче ми вводимо поняття canonical_qubits як засіб для належного оновлення порядку кубітів при передачі його до Executor, у результаті того, як порядок кубітів фіксується під час розміщення схем у блоках та пошуку унікальних інструкцій. Дивіться документацію Угода про порядок кубітів для деталей.

# Determine the canonical qubits order
meas_box = boxed_circuit.data[-1]
canonical_qubits = [
idx
for idx, qubit in enumerate(boxed_circuit.qubits)
if qubit in meas_box.qubits
]

# map canonical qubit to physical (isa) qubit
c_2_p = {c: p for c, p in enumerate(canonical_qubits)}
# map physical (isa) qubit to virtual qubit (index in original circuit)
p_2_v = {p: v for v, p in enumerate(layout)}
# compute map between virtual and canonical qubit indices.
c_2_v = {c: p_2_v[p] for c, p in c_2_p.items()}

assert len(c_2_v) == num_qubits

bases_canon = [
np.array([base_i[c_2_v[c]] for c in range(num_qubits)], dtype=np.uint8)
for base_i in bases_virt
]

Робочий процес для затінення світлового конуса, навчання шуму та введення антишуму

примітка

У цьому туторіалі ми виконуємо обчислення меж SLC до завершення навчання шуму, тому схема, яку потрібно пом'якшити, виконується якомога ближче за часом до навченої моделі шуму. У принципі цей робочий процес можна розпаралелити ще більше: завдання навчання шуму може виконуватися, поки паралельно оцінюються межі шуму. Для довільної квантової схеми обчислення меж шуму може масштабуватися зі слабко експоненційною залежністю, тому розпаралелення обчислення меж (наприклад, на багатьох ядрах CPU) дає точніші межі для заданого бюджету часу обчислень, а виконання на QPU та обчислення меж самі можуть бути розпаралелені для найефективнішого робочого процесу.

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

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

Іншими словами, цей крок передбачає структуру моделі шуму, яку NoiseLearnerV3 пізніше заповнить вивченими швидкостями. Знання цієї структури заздалегідь є тим, що дозволяє нам обчислити пряму та зворотну межі для кожної потенційної помилки ще до того, як навчання шуму навіть відбулося.

noise_model_paulis = generate_noise_model_paulis(
unique_2q_instructions, backend.coupling_map, boxed_circuit
)
noise_model_rates = {ref: None for ref in noise_model_paulis}
a. Обчислення прямих меж

Функція compute_forward_bounds оцінює комутаційні відношення між гейтами в кожному шарі та вищезгаданими термами Паулі з точки зору того, як помилки прямого поширення впливають на бажане спостережуване AA. Для гейтів, що комутують з термами Паулі, нічого не робиться. Для кліффордових гейтів вони просуваються до початку схеми. Для некліффордових гейтів ми апроксимуємо їхній вплив на цільові спостережувані, щоб пізніше визначити пріоритетність для скасування шуму (після того, як усі межі буде об'єднано). Ця межа досягається шляхом спочатку застосування норми L2 (а саме, квадратного кореня із суми квадратів відповідних коефіцієнтів термів Паулі). Коли задіяно занадто багато термів кубітів, ми повертаємося до більш вільної межі, що використовує нерівність трикутника.

Встановлення параметрів обчислення меж

Наступні налаштування керують класичними обчисленнями меж:

  • slc_atol: абсолютна точність обчислень. Терми операторів з коефіцієнтами нижче цього порогу вважаються незначними і відкидаються.

  • slc_eigval_max_qubits: максимальна кількість кубітів, для яких обчислюється тісніша межа на основі власних значень (норма L2). Оператори, підтримувані на більшій кількості кубітів, ніж це, повертаються до вільнішої межі нерівності трикутника, описаної вище.

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

  • slc_num_processes: кількість процесів ЦП, між якими розпаралелюється обчислення меж.

  • slc_timeout: часовий бюджет (у секундах) для обчислення меж. Будь-які шари, недосягнуті в межах цього бюджету, отримують тривіальну межу 2.

примітка

У цьому туторіалі таймаут досягається навмисно: ми навмисно тримаємо slc_timeout невеликим, щоб зошит виконувався швидко, і щоб продемонструвати, що досягнення таймауту є безпечним. Обчислені межі є верхніми межами на кожному етапі, тому досягнення таймауту лише робить межі менш точними (тривіальними) для шарів, яких не було досягнуто; це ніколи не занижує вплив помилки. Це проявляється на візуалізаціях нижче як найлівіші шари, повністю заповнені тривіальною межею. Збільшення таймауту (або кількості процесів) звужує межі та ще більше зменшує накладні витрати на семплування.

slc_atol = 1e-8
slc_eigval_max_qubits = 18
slc_evolution_max_terms = 1000
slc_num_processes = 8
slc_timeout = 60
forward_bounds = compute_forward_bounds(
boxed_circuit,
noise_model_paulis,
isa_observable,
evolution_max_terms=slc_evolution_max_terms,
eigval_max_qubits=slc_eigval_max_qubits,
atol=slc_atol,
num_processes=slc_num_processes,
timeout=slc_timeout,
)
Bounds computation timed out.

Візуалізація SLC для ручної перевірки

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

  • Ми можемо чітко розрізнити два конуси, що виникають з двох нетотожних Паулі у спостережуваному.

  • Ми бачимо, що вимірювання X на кубіті 6 комутує з помилкою X у крайньому правому шарі.

  • Ми бачимо, що Паулі Z на кубіті 13 комутує з помилкою Z у крайньому правому шарі.

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

for p in "XYZ":
display(
draw_shaded_lightcone(
boxed_circuit,
forward_bounds,
noise_model_paulis,
pauli_filter=p,
scale=0.15,
fold=-1,
idle_wires=False,
wire_order=wire_order,
measure_arrows=False,
)
)

Output of the previous code cell

Output of the previous code cell

Output of the previous code cell

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

Далі ми звужуємо межі за допомогою функції tighten_with_speed_limit. Геометричне поширення світлового конуса спостережуваного вже повністю враховано в прямих межах, обчислених вище. Те, що додає ця функція, — це прохід самоузгодженості, який використовує обмежену швидкість, з якою інформація може протікати через схему — світловий конус росте не більше ніж на один кубіт за шар двокубітних гейтів — щоб звузити раніше обчислені межі одна відносно одної. Інформація протікає в обох напрямках:

  1. Від меж комутаторів до меж на зворотно-поширеному спостережуваному: якщо спостережуване комутує з помилкою ZZ на кубіті, то воно не може мати там значної компоненти XX або YY.

  2. Від меж на зворотно-поширеному спостережуваному до меж комутаторів: якщо спостережуване має малу компоненту XX і YY на кубіті, то воно повинно майже комутувати з ZZ там.

forward_bounds_tighter = tighten_with_speed_limit(
forward_bounds, boxed_circuit, noise_model_paulis, isa_observable
)

Візуалізація SLC для ручної перевірки

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

for p in "XYZ":
display(
draw_shaded_lightcone(
boxed_circuit,
forward_bounds_tighter,
noise_model_paulis,
pauli_filter=p,
scale=0.15,
fold=-1,
idle_wires=False,
wire_order=wire_order,
measure_arrows=False,
)
)

Output of the previous code cell

Output of the previous code cell

Output of the previous code cell

c. Обчислення зворотних меж

Ця частина прогнозування шуму оцінює, як помилка на певному шарі може вплинути на вхідний стан ρ\rho. Функція compute_backward_bounds спочатку інвертує схему та видаляє гейти вимірювання, а потім продовжує аналіз, подібний до того, що робився для обчислень прямих меж.

backward_bounds = compute_backward_bounds(
boxed_circuit,
noise_model_paulis,
evolution_max_terms=slc_evolution_max_terms,
num_processes=slc_num_processes,
timeout=slc_timeout,
)

Візуалізація SLC для ручної перевірки

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

  • Ми чітко бачимо, як помилки Z спочатку комутують з початковим станом |0⟩.

  • Лише на кубіті 6, де ми ініціалізуємо власний стан +1 базису X, помилка Z не комутує, тоді як помилка X комутує.

for p in "XYZ":
display(
draw_shaded_lightcone(
boxed_circuit,
backward_bounds,
noise_model_paulis,
pauli_filter=p,
scale=0.15,
fold=-1,
idle_wires=False,
wire_order=wire_order,
measure_arrows=False,
)
)

Output of the previous code cell

Output of the previous code cell

Output of the previous code cell

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

Функція merged_bounds визначає точку в схемі, де перехід від зворотних меж до прямих меж мінімізує загальне оцінене зміщення бажаного спостережуваного. Це зміщення обчислюється як сума внесків зворотних меж для всіх місць шуму до цієї точки, плюс внески прямих меж для всіх місць шуму після неї. Наразі це робиться однаково для всіх кубітів.

Важлива примітка

Оптимальна точка, у якій слід перейти від зворотних до прямих меж, залежить від вивчених швидкостей шуму. На цьому етапі ми ще не вивчили жодних швидкостей: noise_model_rates було ініціалізовано як None для кожного шару, що merge_bounds трактує як однорідні швидкості шуму. Тому показані тут об'єднані межі є лише попереднім переглядом; ми перераховуємо їх з фактичними вивченими швидкостями на кроці 3.

merged_bounds = merge_bounds(
boxed_circuit,
forward_bounds_tighter,
backward_bounds,
noise_model_rates,
)
Missing noise rates. Partitioning backward/forward commutator bounds by assuming uniform error rates.
Optimal spacetime partitioning not implemented!Just partitioning list of noisy boxes.

Візуалізація SLC для ручної перевірки

Після об'єднання зворотних та звужених прямих меж поведінка комбінованих SLC стає зрозумілою:

  • Наведена вище функція показує нам, що обрано розбиття, на якому відбувається перехід від зворотних до звужених прямих меж.

  • Нижче ми бачимо, що SLC тепер містять частково зворотні та частково звужені прямі межі.

for p in "XYZ":
display(
draw_shaded_lightcone(
boxed_circuit,
merged_bounds,
noise_model_paulis,
pauli_filter=p,
scale=0.15,
fold=-1,
idle_wires=False,
wire_order=wire_order,
measure_arrows=False,
)
)

Output of the previous code cell

Output of the previous code cell

Output of the previous code cell

Крок 3: Виконання

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

  1. Вивчити шум за допомогою NoiseLearnerV3.

  2. Виконати схему пом'якшення помилок за допомогою фреймворку samplomatic та Executor.

З обмеженими помилками нашої квантової схеми ми вивчаємо відповідні швидкості шуму, щоб визначити пріоритети нашого бюджету помилок, визначити накладні витрати на вибірку та виконати на QPU.

a. Вивчення швидкостей шуму

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

Ми також свідомо обираємо глибини навчання. Практичний висновок для пом'якшення на основі навчання з samplomatic полягає в тому, що дуже вигідно, щоб найглибша глибина навчання відповідала глибині схеми, яку ви хочете пом'якшити. Оскільки layer_pair_depths NLv3 вимірюються в парах шарів (шар плюс його обернений), ми встановлюємо найглибше значення на половину двокубітної глибини шару схеми.

post_selection_enabled = True
# Match the deepest noise-learning depth to the depth of the circuit being
# mitigated. NLv3 ``layer_pair_depths`` are measured in layer pairs (a layer
# plus its inverse), so the deepest value is half the circuit's two-qubit-layer
# depth. Learning to this depth markedly improves the quality of the mitigation.
#
# We measure the two-qubit-layer depth on the pre-boxed ISA circuit: after
# boxing, every two-qubit gate is hidden inside a full-width ``BoxOp``, so a
# ``num_qubits == 2`` filter on ``boxed_circuit`` matches nothing (and
# ``QuantumCircuit.depth`` does not recurse into boxes).
depth_2q = isa_circuit.depth(lambda instr: instr.operation.num_qubits == 2)
max_layer_pair_depth = depth_2q // 2 # dividing by 2 since we want pairs

# 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}
)

noise_learner_options = {
"num_randomizations": 64,
"shots_per_randomization": 128,
"layer_pair_depths": layer_pair_depths,
"post_selection": {
"enable": post_selection_enabled,
"strategy": "edge",
"x_pulse_type": "rx",
},
"environment": {"job_tags": ["TUT_SLC"]},
}

noise_learner = NoiseLearnerV3(backend, noise_learner_options)
noise_learner_job = noise_learner.run(unique_2q_instructions)
noise_learner_result = noise_learner_job.result()
if post_selection_enabled:
print(
"Minimum fraction of shots kept for noise learning experiments: ",
end="",
)
print(
f"{min([min(d.values()) for d in [nlr.metadata['post_selection']['fraction_kept'] for nlr in noise_learner_result[:2]]]):.2f}"
)
Minimum fraction of shots kept for noise learning experiments: 0.71
# Get a dict mapping each InjectNoise.ref to its learned PauliLindbladMap
refs_2_plm = noise_learner_result.to_dict(
unique_2q_instructions, require_refs=False
)

b.i. Оновлення об'єднаних меж з фактичними вивченими швидкостями шуму

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

merged_bounds = merge_bounds(
boxed_circuit,
forward_bounds_tighter,
backward_bounds,
refs_2_plm,
)
Optimal spacetime partitioning not implemented!Just partitioning list of noisy boxes.

b.ii. Обчислення local_scales для виконання на апаратному забезпеченні

compute_local_scales розглядає кожну можливу помилку шуму в схемі та оцінює, наскільки ця помилка може зміщувати кінцеве вимірювання, а також наскільки дорого було б її виправити. Потім вона ранжує помилки за тим, наскільки варто їх пом'якшувати, і вибирає підмножину, яка максимально зменшує зміщення, залишаючись у межах дозволеного бюджету вартості вибірки (або досягаючи бажаної точності). Результатом є набір масштабних коефіцієнтів, що вказують, які помилки будуть активно пом'якшуватися, а які залишаться непом'якшеними (local_scales), разом із передбаченими загальними накладними витратами на вибірку (sampling_costs) та залишковим зміщенням (residual_bias_bound).

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

id_map = map_modifier_ref_to_ref(boxed_circuit)
summed_rates = 0.0
for box_id, noise_id in id_map.items():
learned_plm = refs_2_plm[noise_id]
summed_rates += np.sum(learned_plm.rates)
# print(f"{box_id}:\tgamma = {np.exp(2 * summed_rates):1.6e}\tsampling cost = {np.exp(4 * summed_rates):1.6e}")
total_gamma = np.exp(2 * summed_rates)
print(
f"Full PEC gamma={total_gamma}, sampling cost (gamma^2) = {total_gamma**2}"
)
Full PEC gamma=12.750876317157042, sampling cost (gamma^2) = 162.58484685543633
biases = []
costs = []
for bias in [0.0] + np.arange(0.001, 0.102, 0.01).tolist():
_, cost_, bias_ = compute_local_scales(
boxed_circuit,
merged_bounds,
refs_2_plm,
sampling_cost_budget=np.inf,
bias_tolerance=bias,
)
biases.append(bias_)
costs.append(cost_)

Компроміс між накладними витратами на вибірку та залишковим зміщенням

xticks = np.arange(0, 11)

fig, ax = plt.subplots()
ax.scatter(
[0], [total_gamma**2], marker="D", c="tab:orange", label="full PEC"
)
ax.plot(
100 * np.array(biases),
np.array(costs),
"o-",
c="tab:blue",
label="PEC+SLC",
)
ax.set_yscale("log")
ax.set_xticks(xticks, [f"{x:.1f}" for x in xticks])

ax.set_xlabel("Remaining bias [%]")
ax.set_ylabel(r"Sampling overhead, $\gamma^2$")
ax.grid()
ax.legend()
fig.suptitle("PEC sampling overhead reduction due to SLC")
Text(0.5, 0.98, 'PEC sampling overhead reduction due to SLC')

Output of the previous code cell

chosen_bias_thres = 0.1
local_scales, sampling_cost, residual_bias_bound = compute_local_scales(
boxed_circuit,
merged_bounds,
refs_2_plm,
sampling_cost_budget=np.inf,
bias_tolerance=chosen_bias_thres,
)
print(
f"PEC+SLC sampling cost (gamma^2) = {sampling_cost} "
f"w/ remaining bias = {100 * residual_bias_bound:.1f}%"
)
PEC+SLC sampling cost (gamma^2) = 48.13646509622307 w/ remaining bias = 10.0%

c. Виконання цільової схеми з антишумом

c.i. Підготовка шаблонної схеми за допомогою samplex

samplex — це вихід методу build Samplomatic, який кодує всю інформацію, необхідну для генерації рандомізованих параметрів для template_circuit. Вони потім використовуються для налаштування об'єктів QuantumProgram, які, у свою чергу, виконуються на QPU за допомогою примітива Executor. Кожен QuantumProgram може містити кілька елементів, які можна розглядати як пару template та samplex.

Дивіться посібник Hello samplomatic для деталей.

# Build template circuit and samplex for later use with the "Executor"
template_circuit, samplex = samplomatic.build(boxed_circuit)
# Set up postselection if it's been enabled
if post_selection_enabled:
# Set up post selection PM (to add PS instructions)
post_selection_pm = PassManager(
[
AddSpectatorMeasures(backend.coupling_map),
AddPostSelectionMeasures(x_pulse_type="rx"),
]
)
final_template_circuit = post_selection_pm.run(template_circuit)
else:
final_template_circuit = template_circuit

c.ii. Налаштування QuantumProgram

num_randomizations = 4096
shots_per_randomization = 64
chunk_size = 256
# Set up QuantumProgram
program = QuantumProgram(shots=shots_per_randomization, noise_maps=refs_2_plm)

# no EM

# Collect up a dict of the other arguments that need to be bound to samplex_inputs
samplex_inputs = {
f"noise_scales.{ref}": float(0) for ref in local_scales.keys()
}
samplex_inputs |= {"basis_changes": {"basis0": bases_canon[0]}}

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

program.append_samplex_item(
circuit=final_template_circuit,
samplex=samplex,
samplex_arguments=samplex_arguments,
shape=(num_randomizations,),
chunk_size=chunk_size,
)

# plain PEC

# Collect a dict of the other arguments that need to be bound to samplex_inputs
samplex_inputs = {
f"noise_scales.{ref}": float(-1) for ref in local_scales.keys()
}
samplex_inputs |= {"basis_changes": {"basis0": bases_canon[0]}}

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

program.append_samplex_item(
circuit=final_template_circuit,
samplex=samplex,
samplex_arguments=samplex_arguments,
shape=(num_randomizations,),
chunk_size=chunk_size,
)

# PEC+SLC

# Collect a dict of the other arguments that need to be bound to samplex_inputs
samplex_inputs = {
f"noise_scales.{ref}": float(-1) for ref in local_scales.keys()
}
samplex_inputs |= {"basis_changes": {"basis0": bases_canon[0]}}
samplex_inputs |= {"local_scales": local_scales}

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

program.append_samplex_item(
circuit=final_template_circuit,
samplex=samplex,
samplex_arguments=samplex_arguments,
shape=(num_randomizations,),
chunk_size=chunk_size,
)

c.iii. Виконання програми за допомогою примітива Executor

executor = Executor(backend)
job_exec = executor.run(program)
results_exec = job_exec.result()

Крок 4: Постобробка

Оскільки ми обчислюємо кінцеве цільове очікуване значення за допомогою executor_expectation_values, ми реалізуємо кілька технік постобробки, щоб допомогти забезпечити отримання найякісніших можливих результатів. Спочатку ми застосовуємо наше твірлінгове усунення помилок зчитування (TREX), яке враховує будь-які помилки, що виникають під час процесу зчитування. Потім ми виправляємо помилки через немарковський шум на наших бекендах Heron за допомогою методу постселекції. Цей метод вимірює активні та кубіти-спостерігачі, потім застосовує повільне обертання до кожного кубіта, а потім вимірює знову. У випадках, коли два вимірювання не підтверджують очікуваний перевернутий кубіт, ці постріли відкидаються шляхом застосування mask з PostSelector. У межах обчислення маски можна встановити конкретну стратегію для фільтрації на основі однокубітних вузлів або сусідніх ребер кубітів-спостерігачів, що може впливати як на кількість відфільтрованих пострілів, так і на якість результатів.

measurement_noise_map = noise_learner_result[2].to_pauli_lindblad_map()
trex_scale_factors = trex_factors(measurement_noise_map, reverser_virt)
post_selection_strategy = "node"
def post_process_conv(datum, steps=16, gamma=None, ps=False, trex=False):
meas = datum["meas"]
flips = datum["measurement_flips.meas"]
signs = datum.get("pauli_signs", None)

meas_basis_axis = None
avg_axis = 0

mask = None
if ps and post_selection_enabled:
# Post-select the results
post_selector = PostSelector.from_circuit(
circuit=final_template_circuit, coupling_map=backend.coupling_map
)

# Compute the ps mask for filtering results
mask = post_selector.compute_mask(
datum, strategy=post_selection_strategy
)

# Compute fraction of shots kept from post selection
total_num_shots = num_randomizations * shots_per_randomization
ps_ratio = np.sum(mask) * 100 / total_num_shots / len(bases_canon)
print(
f"With {post_selection_strategy}-based post selection ({ps_ratio:.1f}% of shots kept):"
)

results = []
for i in range(steps, num_randomizations + 1, steps):
# Compute mitigated expvals w/out post-selection
res = executor_expectation_values(
meas[:i],
reverser_virt,
meas_basis_axis,
avg_axis=avg_axis,
measurement_flips=flips[:i],
pauli_signs=signs[:i] if signs is not None else None,
postselect_mask=mask[:i] if mask is not None else None,
rescale_factors=trex_scale_factors if trex else None,
gamma_factor=gamma,
)
results.append(res[0])
return results
gamma_pec = gamma_from_noisy_boxes(refs_2_plm, id_map)
gamma_slc = gamma_from_noisy_boxes(refs_2_plm, id_map, local_scales)
steps = 16
results = {}

for label, result_idx, gamma, use_ps, use_trex in [
("PEC", 1, gamma_pec, True, True),
("PEC+SLC", 2, gamma_slc, True, True),
("Unmitigated", 0, None, False, False),
]:
res = post_process_conv(
results_exec[result_idx],
steps=steps,
gamma=gamma,
ps=use_ps,
trex=use_trex,
)
results[label] = res
With node-based post selection (24.1% of shots kept):
With node-based post selection (24.2% of shots kept):

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

  • Непом'якшений результат перебуває далеко за межами смуги зміщення 10% (близько 0,35) і не залежить від кількості рандомізацій.

  • На цьому пристрої повний PEC несе накладні витрати на вибірку γ21.6×102\gamma^2 \approx 1.6\times10^2. Після великих ранніх коливань оцінка звичайного PEC відновлює сигнал і встановлюється приблизно на 1,1, дещо перевищуючи точне значення. Зауважте, що смуга зміщення 10% застосовується лише до оцінювача PEC+SLC: звичайний PEC у принципі непом'якшений, тому це залишкове відхилення не покривається гарантією зміщення, а натомість відображає статистичні флуктуації та недоліки вивченої моделі шуму, що є прийнятним для такого розміру задачі.

  • SLC зменшує накладні витрати ще приблизно у 3,4 рази (до γ248\gamma^2 \approx 48, для межі залишкового зміщення близько 10%). PEC+SLC також сильно коливається при невеликій кількості рандомізацій, спочатку навіть досягаючи нефізичної області, але його оцінка стабільно сходиться до точного значення, встановлюючись близько 1,0 добре в межах смуги. Він відповідає або перевищує точність звичайного PEC приблизно за третину вартості вибірки, демонструючи перевагу затінення світлового конуса.

  • Похибки обох пом'якшених оцінок звужуються з накопиченням рандомізацій; при фіксованій кількості рандомізацій оцінювач PEC+SLC має меншу статистичну дисперсію, що узгоджується з його меншим γ2\gamma^2.

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

fig, ax = plt.subplots(1, 1, figsize=(12, 6))

ax.axhline(1.0, color="black", label="Exact")
ax.fill_between(
[-50, 4100], -10, 0, color="grey", alpha=0.25, label="Unphysical"
)
ax.fill_between([-50, 4100], 1, 10, color="grey", alpha=0.25)
ax.fill_between(
[-50, 4100], 0.9, 1.1, color="red", alpha=0.25, label="10% bias"
)

for label, res in results.items():
ax.errorbar(
list(range(steps, num_randomizations + 1, steps)),
[r[0] for r in res],
yerr=[r[1] for r in res],
alpha=0.75,
marker="o",
linestyle="",
markerfacecolor="none",
label=label,
)

ax.set_ylabel(r"$\langle X_{6}Z_{13}\rangle$")
ax.set_xlabel("# randomizations")
ax.grid()

ax.legend(ncols=2)
ax.set_ylim([-0.1, 2.0])
ax.set_xlim([-50, 4100])
(-50.0, 4100.0)

Output of the previous code cell

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

Якщо ця робота вас зацікавила, вам може бути цікавий наступний матеріал: