Grupowanie wyrazów operatora: wykorzystaj strukturę operatora
Grupowanie jest jedną z operacji w qiskit_fermions.operators.terms, który zbiera procedury dzielące wyrazy operatora na podstawie ich struktury.
Jak opisano w przewodniku o operatorach, operatory mogą przechowywać tablicę grup, opcjonalną część rzadkiej struktury danych, która przypisuje każdemu wyrazowi indeks grupy. Wyrazy o tym samym indeksie grupy tworzą grupę. Dzięki temu możesz systematycznie wykorzystywać tę strukturę w dalszym przetwarzaniu.
Grupując powiązane wyrazy (według własności fizycznych, zależności algebraicznych lub symetrii specyficznych dla problemu), możesz uzyskać kilka korzyści:
-
Zoptymalizowana synteza obwodów: zgrupowane wyrazy niosą informacje specyficzne dla problemu przez cały stos, co pozwala łatwiej optymalizować syntezowane obwody. Skutkuje to mniejszą głębokością obwodu i liczbą bramek.
-
Zachowanie struktury fizycznej: grupy kodują znaczącą strukturę wyjściowego problemu (na przykład wzorce oddziaływań lub symetrie), którą można wykorzystać do bardziej fizycznie sensownych dekompozycji.
-
Wybór zbiorów przepływu: grupowanie może w naturalny sposób kodować zbiory przepływu i inne struktury grafowe istotne dla symulacji kwantowej, jak pokazano w [1].
-
Lepsza stabilność algorytmiczna: jak pokazano w procedurze SqDRIFT, niektóre algorytmy mogą zyskać na stabilności lub wydajności dzięki zachowaniu własności fizycznych.
-
Czynniki wzoru produktowego: fermionowy wzór produktowy dzieli operator na jego grupy, więc grupowanie określa zarówno koszt, jak i błąd Trottera ewolucji w czasie. Zobacz Synteza bramki Evolution w przestrzeni fermionowej.
Użycie
Najprostszym sposobem zdefiniowania grupowania operatora jest ustawienie atrybutu groups operatora docelowego. Poniżej znajduje się przykład grupowania wyrazów według zbiorów przepływu linii, jak pokazano na rysunku 1c w [1].
The qiskit_fermions.operators.terms.grouping module provides convenience methods for automatically identifying structure in an operator and assigning group indices accordingly.
>>> 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>
To prosta siatka dwa na trzy modów Majorany połączonych krawędziami skierowanymi. Każda krawędź reprezentuje wyraz operatora działający na dwa połączone mody. Celem jest pogrupowanie tych wyrazów według etykiet krawędzi (pokazanych na grafie). Wyrazy połączone krawędziami o tej samej etykiecie trafiają do tej samej grupy. Poniższy kod pokazuje, jak to zrobić:
- 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);
Analiza grupowania
Grupy nie narzucają własnego znaczenia: tablica grup zapisuje, które wyrazy należą do siebie, ale nigdy dlaczego. Żadna procedura w tej bibliotece nie raportuje, czy grupowanie jest „poprawne”, ponieważ nie ma jednego pojęcia poprawności, względem którego można by to oceniać. Dalsze etapy przetwarzania zakładają jednak pewne własności grupowania, a funkcje z tej sekcji pozwalają sprawdzić takie założenie z góry, zamiast płacić za nie przy każdym wywołaniu.
Najczęstsza konwencja zakłada, że każda grupa jest osobno hermitowska. To właśnie umożliwia fermionowe losowe wzory produktowe: pojedynczy z nie jest hermitowski, więc jego ewolucja w czasie nie jest unitarna i nie można go próbkować osobno. Sparowanie go z daje grupę, którą można próbkować.
- 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);
Wymuszany jest jeden indeks grupy na wyraz
Tablica grup przechowuje jeden indeks na wyraz, a przypisanie tablicy o innej długości zgłasza błąd. Żaden dalszy etap nie sprawdza ponownie tego niezmiennika, więc jest on wymuszany tam, gdzie indeksy trafiają do operatora: zbyt krótka tablica po cichu pomijałaby końcowe wyrazy, a zbyt długa wymyślałaby grupy, których nie niesie żaden wyraz.
>>> 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
Czyszczenie indeksów grup przez przypisanie None jest zawsze dozwolone.
Te dwa sprawdzenia są niezależne
groups_have_uniform_coeffs() raportuje, czy współczynniki grupy są zgodne, co jest założeniem, które przyjmuje group_coeff_means(), uśredniając moduły. Nie jest to słabsza forma sprawdzenia hermitowskości: żadne z tych sprawdzeń nie implikuje drugiego, w żadnym kierunku.
>>> 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])
Domyślnie porównywane są moduły, ponieważ właśnie je uśrednia group_coeff_means(). Hermitowsko sprzężona para o współczynnikach zespolonych ma równe moduły, ale nierówne współczynniki, więc spełnia tylko formę domyślną.
>>> 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() dziedziczy jednostronną gwarancję is_hermitian(): wpis True jest zawsze wiarygodny, natomiast wpis False jest wiarygodny tylko dla typów operatorów, których postać normalna jest prawdziwą postacią kanoniczną. Sprawdź dokumentację konkretnego typu operatora, aby dowiedzieć się, który przypadek ma zastosowanie.
A. Gandon et al., Stabilizer-based quantum simulation of fermion dynamics with local qubit encodings, arXiv:2512.11418v2.