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

Групування доданків оператора: використання структури оператора

Групування є однією з операцій у qiskit_fermions.operators.terms, де зібрано процедури, що розбивають доданки оператора за їхньою структурою.

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

Групуючи пов'язані доданки (за фізичними властивостями, алгебраїчними співвідношеннями чи симетріями, специфічними для задачі), ти отримуєш кілька переваг:

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

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

  • Вибір потокових множин: Групування може природно кодувати потокові множини та інші теоретико-графові структури, важливі для квантової симуляції, як показано в [1].

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

  • Множники формули добутку: Ферміонна формула добутку розбиває оператор на його групи, тож групування визначає і вартість, і похибку Троттера для часової еволюції. Див. Синтез гейта Evolution у ферміонному просторі.

Використання​

Найпростіший спосіб задати групування оператора — встановити атрибут groups цільового оператора. Нижче наведено приклад групування доданків за потоковими множинами ліній, як показано на рис. 1c у [1].

Підказка

Модуль qiskit_fermions.operators.terms.grouping надає зручні методи для автоматичного виявлення структури в операторі та відповідного призначення індексів груп.

>>> import rustworkx as rx
>>> graph = rx.PyDiGraph()
>>> _ = graph.add_nodes_from(range(6))
>>> edges = [(0, 1), (1, 2), (3, 0), (1, 4), (5, 2), (5, 4), (4, 3)]
>>> groups = [0, 0, 1, 2, 1, 3, 3]
>>> edges_with_group_as_payload = [(i, j, g) for (i, j), g in zip(edges, groups)]
>>> _ = graph.add_edges_from(edges_with_group_as_payload)
>>> from rustworkx.visualization import mpl_draw
>>> mpl_draw(
... graph,
... pos={i: (i % 3, i // 3) for i in range(6)},
... edge_labels=str,
... with_labels=True,
... )
<Figure size ... with 1 Axes>

A simple directed graph on which to define a MajoranaOperator.

Це проста гратка два на три з мод Майорани, з'єднаних напрямленими ребрами. Кожне ребро представляє доданок оператора, що діє на дві з'єднані моди. Мета — згрупувати ці доданки за мітками їхніх ребер (показані на графі). Доданки, з'єднані ребрами з однаковою міткою, потрапляють в одну групу. Наступний код показує, як це зробити:

>>> from qiskit_fermions.operators import MajoranaOperator
>>>
>>> # Construct operator directly using sparse arrays
>>> # 7 terms, each with coefficient 1.0, modes from edge endpoints
>>> op = MajoranaOperator(
... coeffs=[1.0] * 7,
... modes=[node for edge in edges for node in edge],
... boundaries=[0, 2, 4, 6, 8, 10, 12, 14],
... )
>>>
>>> # Assign group indices to terms (groups is part of the sparse data structure)
>>> op.groups = groups
>>>
>>> # Partition operator by groups
>>> grouped_ops = op.split_out_groups()
>>>
>>> # Inspect each group
>>> for i, g in enumerate(grouped_ops):
... print(f"Group {i}: {list(sorted(g.iter_terms()))}")
Group 0: [([0, 1], (1+0j)), ([1, 2], (1+0j))]
Group 1: [([3, 0], (1+0j)), ([5, 2], (1+0j))]
Group 2: [([1, 4], (1+0j))]
Group 3: [([4, 3], (1+0j)), ([5, 4], (1+0j))]

Аналіз групування​

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

Найпоширеніша конвенція — кожна група окремо ермітова. Саме це робить взагалі можливими ферміонні рандомізовані формули добутку: поодинокий ap†aqa^\dagger_p a_q з p≠qp \neq q не є ермітовим, тож його часова еволюція не є унітарною, і його не можна семплувати окремо. Поєднання його з aq†apa^\dagger_q a_p дає групу, яку можна.

>>> from qiskit_fermions.operators import FermionOperator
>>> from qiskit_fermions.operators.terms.grouping import (
... group_coeff_means,
... groups_are_hermitian,
... groups_have_uniform_coeffs,
... )
>>>
>>> # a conjugate pair, plus one unpaired term. The operator is built from the sparse
>>> # arrays rather than from a dictionary, because only the former fixes the term order
>>> # that the group indices below are paired with.
>>> pair_and_single = FermionOperator(
... [1.0, 1.0, 1.0],
... [True, False, True, False, True, False],
... [0, 1, 1, 0, 2, 3],
... [0, 2, 4, 6],
... )
>>> pair_and_single.groups = [0, 0, 1]
>>>
>>> groups_are_hermitian(pair_and_single) # group 1 is unpaired
[True, False]
>>> group_coeff_means(pair_and_single)
[1.0, 1.0]

Один індекс групи на доданок є обов'язковим​

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

>>> pair_and_single.groups = [0, 0] # one index short, on a three-term operator
Traceback (most recent call last):
ValueError: expected one group index per term, but got 2 for 3 terms

Очищення індексів груп призначенням None завжди дозволене.

Дві перевірки незалежні​

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

>>> from qiskit_fermions.operators.terms.grouping import (
... groups_are_hermitian,
... groups_have_uniform_coeffs,
... )
>>> # uniform coefficients, but not Hermitian: the terms are not each other's adjoint
>>> uniform = FermionOperator(
... [1.0, 1.0],
... [True, False, True, False],
... [0, 1, 2, 3],
... [0, 2, 4],
... )
>>> uniform.groups = [0, 0]
>>> groups_have_uniform_coeffs(uniform), groups_are_hermitian(uniform)
([True], [False])
>>> # Hermitian, but with mixed magnitudes: two conjugate pairs at different scales
>>> mixed = FermionOperator(
... [1.0, 1.0, 5.0, 5.0],
... [True, False, True, False, True, False, True, False],
... [0, 1, 1, 0, 2, 3, 3, 2],
... [0, 2, 4, 6, 8],
... )
>>> mixed.groups = [0, 0, 0, 0]
>>> groups_are_hermitian(mixed), groups_have_uniform_coeffs(mixed)
([True], [False])

За замовчуванням порівнюються модулі, бо саме їх усереднює group_coeff_means(). Ермітово спряжена пара з комплексними коефіцієнтами має рівні модулі, але нерівні коефіцієнти, тому задовольняє лише форму за замовчуванням.

>>> conjugate = FermionOperator(
... [1.0j, -1.0j],
... [True, False, True, False],
... [0, 1, 1, 0],
... [0, 2, 4],
... )
>>> conjugate.groups = [0, 0]
>>> groups_have_uniform_coeffs(conjugate)
[True]
>>> groups_have_uniform_coeffs(conjugate, abs=False)
[False]
Примітка

groups_are_hermitian() успадковує односторонню гарантію is_hermitian(): запис True завжди надійний, тоді як запис False надійний лише для типів операторів, нормальна форма яких є справжньою канонічною формою. Щоб дізнатися, що застосовується, зверніся до конкретного типу оператора.

[1] (1,2)

A. Gandon et al., Stabilizer-based quantum simulation of fermion dynamics with local qubit encodings, arXiv:2512.11418v2.