Групування доданків оператора: використання структури оператора
Групування є однією з операцій у 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>
Це проста гратка два на три з мод Майорани, з'єднаних напрямленими ребрами. Кожне ребро представляє доданок оператора, що діє на дві з'єднані моди. Мета — згрупувати ці доданки за мітками їхніх ребер (показані на графі). Доданки, з'єднані ребрами з однаковою міткою, потрапляють в одну групу. Наступний код показує, як це зробити:
- PYTHON
- C
>>> 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))]
#include <qiskit_fermions.h>
uint64_t num_terms = 7;
uint64_t num_modes = 14;
uint32_t modes[14] = {0, 1, 1, 2, 3, 0, 1, 4, 5, 2, 5, 4, 4, 3};
QkComplex64 coeffs[7] = {{1.0, 0.0}, {1.0, 0.0}, {1.0, 0.0}, {1.0, 0.0},
{1.0, 0.0}, {1.0, 0.0}, {1.0, 0.0}};
uint32_t boundaries[8] = {0, 2, 4, 6, 8, 10, 12, 14};
QfMajoranaOperator *op = qf_maj_op_new(num_terms, num_modes, coeffs, modes, boundaries);
uint32_t groups[7] = {0, 0, 1, 2, 1, 3, 3};
qf_maj_op_set_groups(op, groups, num_terms);
QfMajoranaOperator *group_ops[4];
qf_maj_op_split_out_groups(op, NULL, 0, group_ops);
Аналіз групування
Групи не приписують жодного власного сенсу: масив груп фіксує, які доданки належать разом, але ніколи не пояснює чому. Жодна процедура в цій бібліотеці не повідомляє, чи є групування «правильним», бо не існує єдиного поняття правильності, щодо якого можна було б звітувати. Проте подальші споживачі припускають певні властивості групування, а функції цього розділу дають змогу перевірити таке припущення заздалегідь, а не платити за нього при кожному виклику.
Найпоширеніша конвенція — кожна група окремо ермітова. Саме це робить взагалі можливими ферміонні рандомізовані формули добутку: поодинокий з не є ермітовим, тож його часова еволюція не є унітарною, і його не можна семплувати окремо. Поєднання його з дає групу, яку можна.
- PYTHON
- C
>>> 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]
#include <qiskit_fermions.h>
uint64_t num_terms = 3;
uint64_t num_actions = 6;
bool actions[6] = {true, false, true, false, true, false};
uint32_t modes[6] = {0, 1, 1, 0, 2, 3};
QkComplex64 coeffs[3] = {{1.0, 0.0}, {1.0, 0.0}, {1.0, 0.0}};
uint32_t boundaries[4] = {0, 2, 4, 6};
QfFermionOperator *op =
qf_ferm_op_new(num_terms, num_actions, coeffs, actions, modes, boundaries);
uint32_t groups[3] = {0, 0, 1};
qf_ferm_op_set_groups(op, groups, num_terms);
bool hermitian[2];
qf_ferm_op_groups_are_hermitian(op, 1e-8, hermitian);
double means[2];
qf_ferm_op_group_coeff_means(op, means);
Один індекс групи на доданок є обов'язковим
Масив груп містить один індекс на доданок, і призначення масиву іншої довжини викликає помилку. Подальші етапи не перевіряють цей інваріант повторно, тому його забезпечують там, де індекси потрапляють в оператор: надто короткий масив мовчки відкинув би кінцеві доданки, а надто довгий вигадав би групи, яких не несе жоден доданок.
>>> 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 надійний лише для типів операторів, нормальна форма яких є справжньою канонічною формою. Щоб дізнатися, що застосовується, зверніся до конкретного типу оператора.
A. Gandon et al., Stabilizer-based quantum simulation of fermion dynamics with local qubit encodings, arXiv:2512.11418v2.