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

Як обчислити власні межі

compute_forward_bounds і compute_backward_bounds — це зручні обгортки. Обидві побудовано з тих самих трьох частин, які відкриває публічний API:

  • Функція норми, що обмежує окремий еволюціонований член похибки Паулі

  • Світловий конус, що задає, до чого еволюціонують члени похибки

  • compute_bounds, що ітерує схему та розподіляє роботу за членами

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

Що насправді роблять обгортки​

Дві обгортки відрізняються в трьох місцях, перелічених вище:

compute_forward_boundscompute_backward_bounds
схемаяк заданоremove_measure(circuit).inverse()
функція нормиtime_evolved_norm_forward, щодо спостережуваноїtime_evolved_norm_backward, щодо початкового стану
світловий конусLightCone.initialize_from_pauli, засіяний зі спостережуваноїLightCone.initialize_from_measurements, засіяний з кожного активного кубіта
backwardsFalseTrue

Пряма межа запитує «Наскільки ця похибка може змінити спостережувану, яку я збираюся виміряти?» і відповідає на це спектральною нормою комутатора. Зворотна межа запитує «Наскільки ця похибка може змінити стан, з якого я почав?» і відповідає ядерною нормою, беручи цей стан за нульовий стан на кожному активному кубіті. Це різні запитання, і кожне з них є правильним для похибок у своїй половині схеми.

Випадок дзеркальної схеми​

Дзеркальна схема застосовує певний унітарний оператор, а потім скасовує його: U†UU^\dagger U. Вони з'являються щоразу, коли ідеальний результат має бути відомий точно (загалом ехо Лошмідта, рандомізоване бенчмаркінгове тестування та виведені перевірки).

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

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

_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_slc": "qiskit-addon-slc", "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")
import numpy as np
from qiskit import QuantumCircuit
from qiskit.quantum_info import Pauli
from qiskit_addon_slc.utils import generate_noise_model_paulis
from samplomatic.transpiler import generate_boxing_pass_manager
from samplomatic.utils import find_unique_box_instructions

def trotter_ising_circuit(num_qubits, num_steps, rx_angle, rzz_angle):
"""Trotterized transverse-field Ising evolution on a 1D chain."""
circuit = QuantumCircuit(num_qubits)
for _ in range(num_steps):
circuit.rx(rx_angle, range(num_qubits))
circuit.barrier()
for start in (0, 1): # even then odd bonds
for i in range(start, num_qubits - 1, 2):
circuit.rzz(rzz_angle, i, i + 1)
circuit.barrier()
return circuit

num_qubits = 10
# The circuit of the quickstart guide...
forward_half = trotter_ising_circuit(
num_qubits, num_steps=4, rx_angle=np.pi / 16, rzz_angle=-np.pi / 3
)
# ...followed by its inverse. The ideal result is the initial state.
circuit = forward_half.compose(forward_half.inverse())

observable = Pauli("I" * num_qubits).compose("Z", [num_qubits // 2])

boxing_pass = generate_boxing_pass_manager(
inject_noise_targets="all",
inject_noise_strategy="individual_modification",
inject_noise_site="after",
twirling_strategy="active",
remove_barriers="never",
)
boxed_circuit = boxing_pass.run(circuit)
noise_model_paulis = generate_noise_model_paulis(
find_unique_box_instructions(boxed_circuit)
)

print(f"mirror circuit: {num_qubits} qubits, depth {circuit.depth()}")
print(f"noisy layers: {len(noise_model_paulis)}")
mirror circuit: 10 qubits, depth 24
noisy layers: 4

Збираємо власну межу​

Рецепт зберігає те, що compute_backward_bounds робить зі схемою (прибирає вимірювання, обертає її та ітерує з backwards=True), і замінює дві інші частини: пряму функцію норми та світловий конус, засіяний зі спостережуваної через LightCone.initialize_from_pauli, точно так, як compute_forward_bounds засіває свій. Кожна заміна — це один рядок.

from functools import partial

from qiskit_addon_slc.bounds import (
LightCone,
compute_bounds,
time_evolved_norm_forward,
)
from qiskit_addon_slc.utils import remove_measure

def compute_mirror_backward_bounds(
circuit,
noise_model_paulis,
/,
observable,
*,
evolution_max_terms=1_000_000,
**kwargs,
):
"""Bounds the backward direction of a mirror circuit with a forward-style bound.

Valid only for circuits whose second half inverts the first: the backward evolution then
traverses the same gates as the forward one, and reproduces the forward bounds.
"""
# As `compute_backward_bounds` does: strip measurements and invert, so that walking the circuit
# backwards from its end becomes walking the inverted circuit forwards.
circuit = remove_measure(circuit).inverse()

# The substitution: the forward norm function, bounding against the observable rather than
# against the initial state.
norm_fn = partial(
time_evolved_norm_forward,
observable=observable,
evolution_max_terms=evolution_max_terms,
)

# Seed the light cone from the observable, not from every active qubit.
light_cone = LightCone.initialize_from_pauli(circuit, observable)

return compute_bounds(
circuit,
noise_model_paulis,
light_cone,
norm_fn,
backwards=True,
**kwargs,
)

Що це дає​

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

from qiskit_addon_slc.bounds import (
compute_backward_bounds,
compute_forward_bounds,
)

forward_bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable
)
stock_backward = compute_backward_bounds(boxed_circuit, noise_model_paulis)
custom_backward = compute_mirror_backward_bounds(
boxed_circuit, noise_model_paulis, observable
)

def summarize(label, bounds):
"""Prints a one-line summary of a set of bounds."""
rates = np.concatenate([bound.rates for bound in bounds.values()])
# The bounds are floating-point results, so a term counts as bounded by 0, or as left at the
# trivial 2.0, when it is within rounding error of that value.
exact = np.isclose(rates, 0.0, rtol=0.0, atol=1e-12)
trivial = np.isclose(rates, 2.0, rtol=0.0, atol=1e-12)
computed = ~exact & ~trivial
print(
f"{label:18} mean {rates.mean():.4f} ± {rates.std():.3f} │ "
f"at 0: {exact.mean():4.0%} computed: {computed.mean():4.0%} "
f"at 2.0: {trivial.mean():4.0%}"
)

summarize("forward", forward_bounds)
summarize("backward (stock)", stock_backward)
summarize("backward (custom)", custom_backward)
forward mean 0.4323 ± 0.679 │ at 0: 38% computed: 61% at 2.0: 1%
backward (stock) mean 1.7218 ± 0.592 │ at 0: 0% computed: 95% at 2.0: 5%
backward (custom) mean 0.4323 ± 0.679 │ at 0: 38% computed: 61% at 2.0: 1%

Стандартна зворотна межа лежить поблизу тривіального значення 2.0, що майже не несе інформації. На дзеркальній схемі кожна похибка «близька до початкового стану» у сенсі, який вимірює ця межа, тож вона не може розрізнити члени. Власна межа в кілька разів щільніша і обмежує багато членів нулем.

Щоб перевірити реалізацію, ці дві межі можна порівняти бокс за боксом. Для кожного боксу власна зворотна межа виходить тотожною прямій межі того самого боксу з точністю до похибки заокруглення. Обидві обмежують ту саму похибку в тій самій точці схеми, а на дзеркальній схемі вентилі після цієї точки скорочуються до оберненого до вентилів перед нею, тож обидва обчислення оцінюють ту саму величину з протилежних кінців:

matches = sum(
np.allclose(forward_bounds[box_id].rates, custom_backward[box_id].rates)
for box_id in forward_bounds
)
print(f"{matches} of {len(forward_bounds)} boxes match the forward bounds")

worst = max(
np.abs(forward_bounds[box_id].rates - custom_backward[box_id].rates).max()
for box_id in forward_bounds
)
print(f"largest disagreement across all terms: {worst:.2e}")
16 of 16 boxes match the forward bounds
largest disagreement across all terms: 6.00e-15

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

Передаємо це решті робочого процесу​

Результат — звичайний словник Bounds, тож усе подальше залишається без змін.

На дзеркальній схемі також немає причин обчислювати будь-яку з меж для всієї схеми. Прямі межі потрібні лише для боксів U†U^\dagger, рахуючи з кінця, а зворотні межі лише для боксів UU, рахуючи з початку. Тому max_num_boxes обмежує кожну з них половиною шумових боксів, а merge_bounds об'єднує дві половини.

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

from qiskit.quantum_info import PauliLindbladMap, QubitSparsePauliList
from qiskit_addon_slc.bounds import compute_local_scales, merge_bounds

# Stand-in for rates that a noise-learning experiment would measure on hardware.
rng = np.random.default_rng(42)
noise_rates = {
layer: PauliLindbladMap.from_components(
rng.random(len(terms)) * 5e-3,
QubitSparsePauliList.from_sparse_list(
terms.to_sparse_list(), terms.num_qubits
),
)
for layer, terms in noise_model_paulis.items()
}

# Compute each bound for its own half of the mirror circuit only. `forward_bounds` has an entry
# for every noisy box, which are the boxes `max_num_boxes` counts.
half = len(forward_bounds) // 2
half_forward = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable, max_num_boxes=half
)
half_stock = compute_backward_bounds(
boxed_circuit, noise_model_paulis, max_num_boxes=half
)
half_custom = compute_mirror_backward_bounds(
boxed_circuit, noise_model_paulis, observable, max_num_boxes=half
)

print(
f"each bound computed for {half} of {len(forward_bounds)} noisy boxes\n"
)
print(f"{'bounds':34} {'overhead':>12} {'residual bias':>14}")
for label, bounds in (
(
"forward + stock backward",
merge_bounds(boxed_circuit, half_forward, half_stock, noise_rates),
),
(
"forward + custom backward",
merge_bounds(boxed_circuit, half_forward, half_custom, noise_rates),
),
("forward only, whole circuit", forward_bounds),
):
_, cost, bias = compute_local_scales(
boxed_circuit, bounds, noise_rates, bias_tolerance=0.05
)
print(f"{label:34} {cost:12.4f} {bias:14.4f}")
Optimal spacetime partitioning not implemented! Just partitioning list of noisy boxes.
Optimal spacetime partitioning not implemented! Just partitioning list of noisy boxes.
each bound computed for 8 of 16 noisy boxes

bounds overhead residual bias
forward + stock backward 29848.0583 0.0498
forward + custom backward 465.4028 0.0498
forward only, whole circuit 465.4028 0.0498

Зі стандартними зворотними межами накладні витрати на вибірку набагато більші. merge_bounds використовує зворотні межі до однієї точки перемикання в схемі, а після неї — прямі межі, обираючи точку, що мінімізує загальний зсув. Коли кожну межу обчислено для однієї половини, ця точка припадає на середину схеми, тож бокси UU покриваються лише зворотними межами. Стандартна зворотна межа там близька до тривіального 2.0, що підвищує накладні витрати. Власна настільки ж щільна, якою була б пряма межа, тож накладні витрати збігаються з накладними витратами повних прямих меж.

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

Тому економія зростає з глибиною схеми. Що важливіше, повні прямі межі перестають бути обчислюваними задовго до того, як це стається з половинними. Щойно еволюції ранніх боксів перевищують evolution_max_terms або eigval_max_qubits, їхні межі обрізаються або повертаються до нерівності трикутника, а зрештою їх обчислення коштує надто дорого. Де б ця точка не лежала, підходу «навпіл» потрібно лише, щоб вона припала десь у першу половину схеми. Прямі межі мають сягнути не далі середини, а на дзеркальній схемі зворотні межі для боксів UU еволюціонують свої похибки назад до початку через стільки ж схеми, скільки прямі межі для відповідних боксів U†U^\dagger — до кінця, тож вони вичерпуються на тій самій глибині.

Примітка: Навіщо обчислювати зворотні межі для боксів UU, коли прямі межі боксів U†U^\dagger нібито мають містити ті самі числа? Бо бокси не є дзеркальними образами одне одного. Кожен бокс вносить свій шум з одного боку своїх вентилів, а обернені шари U†U^\dagger — це окремі бокси з власними моделями шуму, тож бокс, що дзеркально відповідає боксу UU, бачить його похибки в іншій точці схеми. Тому їхні межі лише частково збігаються, і межі для боксів UU слід обчислювати саме для цих боксів.

Написання інших власних меж​

Ті самі три заміни охоплюють інші випадки:

  • Інша норма. time_evolved_norm_forward приймає comm_norm_order; передавання будь-чого, окрім 2, пропускає розв'язувач власних значень і обчислює цю норму безпосередньо.

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

  • Зовсім інша функція норми. compute_bounds вимагає лише callable: що приймає Pauli та RotationGates і повертає CommutatorBounds. Працює будь-що, що задовольняє цей підпис, зокрема межа в замкненій формі, специфічна для твого сімейства схем.

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