연산자 항 그룹화: 연산자 구조 활용
그룹화는 연산자 항을 구조에 따라 분할하는 루틴을 모아 둔 qiskit_fermions.operators.terms의 기능 중 하나입니다.
연산자 가이드에서 설명했듯이, 연산자는 각 항을 그룹 인덱스와 연결하는 희소 데이터 구조의 선택적 부분인 그룹 배열을 저장할 수 있습니다. 같은 그룹 인덱스를 공유하는 항들이 하나의 그룹을 이룹니다. 이를 통해 후속 처리에서 이 구조를 체계적으로 활용할 수 있습니다.
관련된 항들을 (물리적 성질, 대수적 관계, 문제별 대칭성 등에 따라) 그룹화하면 여러 이점을 얻을 수 있습니다.
-
최적화된 회로 합성: 그룹화된 항은 문제별 정보를 스택 전체에 전달하여 합성된 회로를 더 간단히 최적화할 수 있게 합니다. 그 결과 회로 깊이와 Gate 수가 줄어듭니다.
-
물리적 구조 보존: 그룹은 원래 문제의 의미 있는 구조(예: 상호작용 패턴이나 대칭성)를 담고 있어, 물리적으로 더 의미 있는 분해에 활용할 수 있습니다.
-
플로 집합 선택: 그룹화는 [1]에서 보듯이 플로 집합과 양자 시뮬레이션에 관련된 기타 그래프 이론적 구조를 자연스럽게 인코딩할 수 있습니다.
-
알고리즘 안정성 향상: SqDRIFT 루틴에서 보듯이, 일부 알고리즘은 물리적 성질의 보존 덕분에 안정성이나 성능이 향상될 수 있습니다.
-
곱 공식 인자: 페르미온 곱 공식은 연산자를 그룹별로 분할하므로, 그룹화가 시간 발전의 비용과 Trotter 오차를 모두 결정합니다. 페르미온 공간에서 Evolution Gate 합성을 참고하세요.
사용법
연산자 그룹화를 정의하는 가장 간단한 방법은 대상 연산자의 groups 속성을 설정하는 것입니다. 아래는 [1]의 그림 1c에 나온 선 플로 집합에 따라 항을 그룹화하는 예제입니다.
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>
이것은 방향이 있는 에지로 연결된 마요라나 모드의 단순한 2x3 격자입니다. 각 에지는 연결된 두 모드에 작용하는 연산자의 한 항을 나타냅니다. 목표는 이 항들을 에지 레이블(그래프에 표시됨)에 따라 그룹화하는 것입니다. 같은 레이블의 에지로 연결된 항은 같은 그룹에 배치됩니다. 다음 코드는 이를 수행하는 방법을 보여 줍니다.
- 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.