주 콘텐츠로 건너뛰기
이 프로젝트는 베타 단계입니다. 버그를 발견하셨나요? GitHub 이슈 만들기. 질문이나 아이디어가 있으신가요? Discussions에서 질문하기 — 피드백을 기다립니다!
이 페이지의 번역 품질은 어떻습니까?

연산자 항 그룹화: 연산자 구조 활용

그룹화는 연산자 항을 구조에 따라 분할하는 루틴을 모아 둔 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>

A simple directed graph on which to define a MajoranaOperator.

이것은 방향이 있는 에지로 연결된 마요라나 모드의 단순한 2x3 격자입니다. 각 에지는 연결된 두 모드에 작용하는 연산자의 한 항을 나타냅니다. 목표는 이 항들을 에지 레이블(그래프에 표시됨)에 따라 그룹화하는 것입니다. 같은 레이블의 에지로 연결된 항은 같은 그룹에 배치됩니다. 다음 코드는 이를 수행하는 방법을 보여 줍니다.

>>> 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))]

그룹화 분석​

그룹 자체에는 정해진 의미가 없습니다. 그룹 배열은 어떤 항이 함께 속하는지만 기록할 뿐 그 이유는 기록하지 않습니다. 이 라이브러리의 어떤 루틴도 그룹화가 "올바른지" 보고하지 않습니다. 기준이 될 단일한 정확성 개념이 없기 때문입니다. 다만 후속 소비자들은 그룹화에 대해 특정 성질을 가정하며, 이 절의 함수들을 사용하면 호출할 때마다 비용을 치르는 대신 그러한 가정을 미리 확인할 수 있습니다.

가장 흔한 규약은 각 그룹이 개별적으로 에르미트라는 것입니다. 바로 이것이 페르미온 무작위 곱 공식을 가능하게 합니다. p≠qp \neq q인 단독 ap†aqa^\dagger_p a_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.