Усічення доданків Паулі під час зворотного поширення
Цей посібник пояснює, як налаштувати механізм усічення доданків Паулі, наданий модулем qiskit_addon_obp.utils.truncating.
Зворотне поширення операторів (OBP) можна використовувати для зменшення глибини квантових схем ціною складнішої спостережуваної. Щоб отримати змістовні результати з OBP, зазвичай потрібно усікати доданки зі спостережуваної, щоб вона не розросталася занадто сильно. Один зі способів дозволити глибше зворотне поширення в схему, не даючи оператору надто розростися, — усікати доданки з малими коефіцієнтами, а не додавати їх до оператора. Усічення доданків може зменшити кількість квантових схем для виконання, але призводить до певної похибки в кінцевому обчисленні очікуваного значення, пропорційної величині коефіцієнтів усічених доданків.
Метод backpropagate приймає необов'язковий TruncationErrorBudget, який налаштовує усічення доданків Паулі з малою вагою для кожної спостережуваної після успішного зворотного поширення кожного зрізу. Кількість усічених доданків залежить від різних параметрів конфігурації, заданих користувачем. На цей момент доступна лише одна стратегія усічення — метод truncate_binary_search. За заданою спостережуваною та певним бюджетом він виконує двійковий пошук за доданками Паулі та коефіцієнтами в цій спостережуваній, щоб знайти оптимальний поріг, такий що сума усічених коефіцієнтів максимальна, але менша за бюджет.
Примітка: За замовчуванням для оцінки та обмеження похибки усічення використовується L1-норма; проте параметр p_norm дозволяє задати Lp-норму, яку слід використовувати.
Більше інформації про те, як користуватися цим параметром, дивись у посібнику Use different Lp-norms for Pauli term truncation.
Наведені нижче приклади ілюструють різні способи побудови TruncationErrorBudget за допомогою супровідної функції setup_budget.
Побудова прикладу схеми
У цьому посібнику використовуються такі зрізи схеми:
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "qiskit": "qiskit", "qiskit_addon_obp": "qiskit-addon-obp", "qiskit_addon_utils": "qiskit-addon-utils", "rustworkx": "rustworkx"}
_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 rustworkx.generators
from qiskit.synthesis import LieTrotter
from qiskit_addon_utils.problem_generators import (
PauliOrderStrategy,
generate_time_evolution_circuit,
generate_xyz_hamiltonian,
)
from qiskit_addon_utils.slicing import combine_slices, slice_by_gate_types
# Generate a linear chain of 10 qubits
linear_chain = rustworkx.generators.path_graph(10)
# Use an arbitrary XY model
hamiltonian = generate_xyz_hamiltonian(
linear_chain,
coupling_constants=(0.05, 0.02, 0.0),
ext_magnetic_field=(0.02, 0.08, 0.0),
pauli_order_strategy=PauliOrderStrategy.InteractionThenColor,
)
# Evolve for some time
circuit = generate_time_evolution_circuit(
hamiltonian, synthesis=LieTrotter(reps=3), time=2.0
)
# slice the circuit by gate type
slices = slice_by_gate_types(circuit)
# for visualization purposes, recombine the slices with barriers between them and draw the resulting circuit
combine_slices(slices, include_barriers=True).draw("mpl", fold=50, scale=0.6)

Ми розглянемо одну просту спостережувану:
from qiskit.quantum_info import SparsePauliOp
obs = SparsePauliOp("IIIIIZIIII")
Найпростіший випадок: фіксований бюджет усічення для кожного зрізу
Доступний бюджет для усічення доданків Паулі може змінюватися на кожному кроці зворотного поширення. Щоб зрозуміти, як це працює, спершу розглянемо найпростіший випадок фіксованого бюджету усічення, заданого користувачем.
Найпростіший спосіб задати бюджет усічення — використати аргумент max_error_per_slice.
Дійсно, саме так зроблено в посібнику Reducing depth of circuits with operator backpropagation. Якщо задати max_error_per_slice як float, кожен зріз отримує бюджет, що дорівнює цьому значенню.
У прикладі нижче ми задаємо це значення 0.001, що гарантує неявну похибку усічення не більше 0.018, якби було зворотно поширено всі 18 зрізів.
Зауваж, що будь-який залишок бюджету похибки після зворотного поширення зрізу та усічення доданків з малими коефіцієнтами завжди додається до бюджету похибки наступного зрізу.
from qiskit_addon_obp.utils.truncating import setup_budget
truncation_error_budget = setup_budget(max_error_per_slice=0.001)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.001], max_error_total=inf, p_norm=1)
from qiskit_addon_obp import backpropagate
from qiskit_addon_obp.utils.simplify import OperatorBudget
op_budget = OperatorBudget(max_qwc_groups=10)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 11 circuit slices.
New observable contains 29 terms and 10 commuting groups.
Далі ми використовуємо екземпляр OBPMetadata та інструменти модуля visualization, щоб візуалізувати процес зворотного поширення.
-
Графік угорі ліворуч показує, що в нас достатньо бюджету, щоб почати усікати доданки спостережуваної після зворотного поширення третього зрізу. Починаючи з третього зрізу, ми знаємо, що усікаємо принаймні один доданок з кожного зрізу, який поширюємо назад, бо після кожного зрізу ми вносимо певну похибку усічення.
-
Графік угорі праворуч показує, що бюджет похибки зростає до
.003для третього зрізу. Ми бачимо різке падіння залишку бюджету, що означає, що доданки було усічено зі спостережуваної. Це узгоджується з тим, що ми зробили висновок із графіка угорі ліворуч. -
Графік унизу ліворуч показує, що в міру усічення доданків зі спостережуваної наша загальна накопичена похибка монотонно зростає. Цей графік також підтверджує, що жоден доданок не усікався, доки не було зворотно поширено третій зріз.
-
Графік унизу праворуч показує, що кількість груп Паулі, що комутують, у нашій спостережуваній зросла до заданої межі
10. Цей графік також показує, як зворотне поширення ще одного шару призвело б до того, що наша спостережувана переросла б задану межу, що видно з перетину чорної та червоної ліній.
Зауваж, що на всіх цих графіках вісь x нумерує зворотно поширені зрізи, але оскільки OBP працює з кінцем схеми, slice 1 — це найостанніший зріз, slice 2 — той, що перед ним, і так далі.
from matplotlib import pyplot as plt
from qiskit_addon_obp.utils.visualization import (
plot_accumulated_error,
plot_left_over_error_budget,
plot_num_qwc_groups,
plot_slice_errors,
)
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Явне задання бюджету зрізів
Якщо ти знаєш, як призначити бюджет кожному зрізу так, щоб продуктивність зворотного поширення була оптимальною, можеш явно призначити бюджет для кожного зрізу. Для демонстрації ми призначимо перших трьом зрізам нульовий бюджет і використаємо бюджет .001 на зріз для решти зрізів.
# Zero out the first 3 slices' budgets
max_error_per_slice = [0.0] * 3 + [0.001] * (len(slices) - 3)
truncation_error_budget = setup_budget(
max_error_per_slice=max_error_per_slice
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0, 0.0, 0.0, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001], max_error_total=inf, p_norm=1)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 11 circuit slices.
New observable contains 32 terms and 10 commuting groups.
Прибравши бюджет з перших трьох шарів, ми дістали ситуацію, коли жоден доданок не усікався аж до четвертого шару, що можна підтвердити на трьох із цих графіків. Дещо несподіваним може бути те, що хоча до четвертого зрізу не було передано залишку бюджету, один доданок усе ж було усічено за рахунок виділеного бюджету .001. Це очевидно з графіка угорі ліворуч, але також видно на графіку угорі праворуч, оскільки крива залишку бюджету вирівнюється між третім і четвертим зрізами. Ще одна помітна деталь: починаючи з цього моменту усікався принаймні один доданок, так само як у попередньому прикладі.
Ключовий висновок: хоча в другому прикладі ми внесли меншу похибку усічення завдяки обнуленню бюджетів деяких зрізів, ми змогли зворотно поширити таку саму кількість зрізів, а наша спостережувана містить таку саму кількість груп Паулі, що комутують. Ми можемо підтвердити, що межа нашої похибки менша в другому прикладі, розглянувши графік унизу ліворуч.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Циклічне задання бюджету
Якщо в тебе є схема з певним повторюваним шаблоном, наприклад схема Троттера, ти можеш захотіти задати бюджет для цієї повторюваної підмножини зрізів і щоб цей бюджет використовувався для всіх наступних повторень цих зрізів.
Конкретніше, приклади схема, яку ми використовуємо, має шість зрізів, що повторюються тричі, разом 18 зрізів. Ми довільно призначимо нульовий бюджет однокубітним шарам і шарам RYY, а бюджет .003 — кожному з шарів RXX. Ми спостерігатимемо, як задання бюджету у вигляді послідовності довжини 6 призводить до циклічного застосування бюджету до всіх 18 зрізів.
Знову зауваж, що зрізи зворотно поширюються у зворотному порядку (тобто починаючи з кінця). Тому перший елемент нашого циклічного бюджету фактично використовується для останнього зрізу, другий елемент — для зрізу перед ним, і так далі.
# Specify a length-6 per-slice budget.
# This will be cycled over three times to be applied to the 18 slices
max_error_per_slice = [0.0] * 4 + [0.003] * 2
truncation_error_budget = setup_budget(
max_error_per_slice=max_error_per_slice
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0, 0.0, 0.0, 0.0, 0.003, 0.003], max_error_total=inf, p_norm=1)
op_budget = OperatorBudget(max_qwc_groups=20)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 13 circuit slices.
New observable contains 49 terms and 14 commuting groups.
Як видно на зображеннях угорі ліворуч та унизу ліворуч, усічення не виконувалося протягом перших чотирьох зрізів, оскільки їм не було виділено бюджету похибки. У зрізах п'ять і шість було усічено частину доданків, коли з'явився бюджет.
Як видно на зображенні угорі ліворуч, після зворотного поширення зрізу сім було внесено порівняно велику похибку, хоча цьому зрізу не було виділено бюджету. Це тому, що з загального бюджету .006, виділеного зрізам п'ять і шість, було використано лише близько .002, тож залишок було передано зрізу сім і здебільшого витрачено, як видно на зображенні угорі праворуч.
Зображення угорі ліворуч та угорі праворуч показують, що невеликий залишок бюджету вичерпується між зрізами 8–10, а новий бюджет з'являється в зрізі 11, як і очікувалося. Зображення угорі ліворуч, угорі праворуч та унизу ліворуч демонструють циклічну поведінку аргументу max_error_per_slice, коли його довжина менша за кількість зрізів. Ця циклічна поведінка тривала б через усі зрізи, але критерій зупинки max_qwc_groups було досягнуто після зворотного поширення 13 зрізів, як видно на зображенні унизу праворуч.
Цікаво також, що кількість груп Паулі фактично зменшується після появи другої порції бюджету в зрізі 11. Це тому, що були групи з малими коефіцієнтами, які не можна було усікти, доки не з'явилося більше бюджету після зворотного поширення зрізу 11, тож вони накопичувалися в спостережуваній протягом кількох ітерацій. Цей випадок також підкреслює, що max_qwc_groups має бути перевищено, щоб алгоритм завершився.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Обмеження загальної похибки
Окрім задання бюджету похибки на зріз, можна задати максимальну похибку, що вноситься усіченням. Щойно цю межу досягнуто, усічення більше не виконується; проте зворотне поширення триватиме, доки спостережувана не стане занадто великою й не буде виконано один із критеріїв зупинки.
Далі ми повторно запускаємо наведений вище експеримент, задавши обмеження max_error_total так, щоб бюджет похибки вичерпався після зворотного поширення зрізу сім.
# Specify a length-6 per-slice budget.
# This will be cycled over 3 times to be applied to the 18 slices
max_error_per_slice = [0.0] * 4 + [0.003] * 2
truncation_error_budget = setup_budget(
max_error_per_slice=max_error_per_slice, max_error_total=0.006
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0, 0.0, 0.0, 0.0, 0.003, 0.003], max_error_total=0.006, p_norm=1)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 10 circuit slices.
New observable contains 67 terms and 20 commuting groups.
Як і очікувалося, наша похибка усічення досягає межі .006 (діаграма унизу ліворуч). Примітно, що в цьому запуску ми не змогли зворотно поширити зріз 11. Це тому, що в нас не вистачило бюджету для усічення доданків, і кількість груп Паулі, що комутують, зросла понад межу 20, як видно на зображенні унизу праворуч.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Може бути бажаним просто обмежити загальний бюджет за допомогою max_error_total, не задаючи max_error_per_slice.
truncation_error_budget = setup_budget(max_error_total=0.018)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.018], max_error_total=0.018, p_norm=1)
Результат комірки вище може трохи здивувати, адже per_slice_budget дорівнює max_error_total. Це означає, що весь доступний бюджет буде витрачено жадібно.
Можна уявити це так: увесь бюджет доступний кожному зрізу (бо ми обходимо per_slice_budget у циклі). Але будь-який уже витрачений бюджет віднімається від бюджету, доступного в цей момент алгоритму.
op_budget = OperatorBudget(max_qwc_groups=10)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 9 circuit slices.
New observable contains 25 terms and 9 commuting groups.
Зображення угорі праворуч показує, як весь бюджет похибки .018 стає доступним першому зрізу. Усічення не виконується до третього зрізу, тому залишок бюджету залишається сталим. Бюджет монотонно зменшується, оскільки повний бюджет стає доступним кожному зворотно поширеному зрізу, доки його не буде витрачено.
Примітно, що в цьому експерименті вдалося зворотно поширити на два зрізи менше, ніж у першому експерименті в цьому ноутбуці, який майже ідентичний. Це демонструє, що для деяких задач рівномірний розподіл бюджету може бути оптимальним. Для інших задач дозвіл зрізам жадібно витрачати весь бюджет може дати кращу продуктивність.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Обмеження кількості зворотно поширених зрізів і загальної похибки разом
Якщо ти не хочеш розподіляти бюджет похибки по всій схемі, але й не хочеш витрачати його жадібно, можна задати кількість зрізів, які ти збираєшся зворотно поширити (num_slices), разом із загальним бюджетом похибки (max_error_total). Це рівномірно розподілить бюджет похибки (відповідно до p_norm) між вхідними зрізами.
Тут ми обмежимо кількість зрізів, які можемо зворотно поширити, значенням 12 і збережемо той самий загальний бюджет похибки.
num_slices = 12
truncation_error_budget = setup_budget(
max_error_total=0.018, num_slices=num_slices, p_norm=1
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0014999999999999998], max_error_total=0.018, p_norm=1)
Тепер ми спробуємо зворотно поширити 12 зрізів, для яких ми виділили бюджет на попередньому кроці. Ми робимо це, просто передаючи останні 12 зрізів нашої схеми. При p_norm=1 кожен із 12 зрізів має доступний бюджет 0.018 / num_slices = 0.0015.
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices[-num_slices:],
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
Оскільки ми передали підмножину наших зрізів (у коді slices[-num_slices:]) до backpropagate, ми маємо об'єднати зрізи, що залишилися після зворотного поширення, із зрізами, які ніколи не надсилалися на зворотне поширення (у коді slices[:-num_slices]).
Після того як ми об'єднали всі зрізи, що залишилися, ми можемо використати combine_slices, щоб згенерувати QuantumCircuit зі зменшеною глибиною. Далі ми перевіряємо, скільки зрізів було зворотно поширено порівняно з тим, наскільки великою стала наша спостережувана.
# Recombine the slices remaining after backprop with the rest of the original circuit
reduced_circuit = combine_slices(slices[:-num_slices] + remaining_slices)
print(f"Backpropagated {num_slices - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 12 circuit slices.
New observable contains 29 terms and 9 commuting groups.
Графіки показують, що нам вдалося успішно виконати зворотне поширення всіх 12 зрізів, утримуючи нашу observable в межах 10 комутівних груп Паулі. Також видно, що використання num_slices разом із max_error_total призводить до розподілу бюджетів між зрізами, а невикористаний бюджет знову передається наступному зрізу. Це найкраще видно на графіку праворуч угорі, де бюджет і витрачається, і поповнюється протягом зворотного поширення.
Примітно, що цей метод розподілу похибки дав найкращі результати (більше зрізів із зворотним поширенням) порівняно з першим експериментом у цьому ноутбуці та з попереднім прикладом. У всіх цих прикладах виділявся бюджет похибки .018, але зворотне поширення працювало по-різному залежно від того, як цей бюджет розподілявся.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Робота з кількома observable
Метод qiskit_addon_obp.backpropagate дозволяє передати послідовність observable. Це спрощує робочий процес, коли є кілька цільових observable.
Ми явно згадуємо це тут, щоб показати, як стратегія усічення обробляє такий випадок. Для цього прикладу ми додаємо до тієї observable, яку використовували досі, ще одну:
obs = [SparsePauliOp("IIIIIZIIII"), SparsePauliOp("IIIIIXIIII")]
Насамкінець ми повторюємо перший експеримент із цього туторіалу, але з двома observable, у які виконується зворотне поширення схеми.
У цьому прикладі це не впливає на кількість зрізів, для яких можна виконати зворотне поширення. Проте видно, що дві observable дали різну кількість доданків Паулі та комутівних груп.
truncation_error_budget = setup_budget(max_error_per_slice=0.001)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.001], max_error_total=inf, p_norm=1)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"The new first observable contains {len(bp_obs[0])} terms and {len(bp_obs[0].group_commuting(qubit_wise=True))} commuting groups."
)
print(
f"The new second observable contains {len(bp_obs[1])} terms and {len(bp_obs[1].group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 11 circuit slices.
The new first observable contains 29 terms and 10 commuting groups.
The new second observable contains 23 terms and 8 commuting groups.
Графіки нижче допомагають проілюструвати, як алгоритм зворотного поширення обробляє кілька observable.
По-перше, графіки ліворуч угорі, праворуч угорі та ліворуч унизу показують, що бюджет для усічення доданків задається окремо для кожної observable. Іншими словами, обидві observable можуть усікати доданки, припускаючи похибку 0.001 на кожен зріз зі зворотним поширенням.
Через різну природу observable це призводить до різного споживання бюджету. У цьому прикладі вони значно перекриваються, що буває не завжди.
Графік праворуч унизу показує, що max_qwc_groups враховує всі observable. Тобто доданки всіх observable групуються разом, щоб отримати одне остаточне число кубітно-комутівних груп, яке порівнюється з max_qwc_paulis. Те саме робиться для порогу max_paulis (який не розглядається в цьому ноутбуці) і який дозволяє встановити ліміт на кількість доданків Паулі в усіх observable.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])
