オペレーター項のグループ化: オペレーター構造を活用する
グループ化は qiskit_fermions.operators.terms 配下の操作の1つで、このモジュールはオペレーターの項をその構造に基づいて分割するルーチンをまとめたものです。
オペレーターガイドで説明したとおり、オペレーターはグループ配列を保持できます。これはスパースなデータ構造のオプション部分で、各項にグループ・インデックスを対応付けます。同じグループ・インデックスを共有する項が1つのグループを形成します。これにより、後続の処理でこの構造を体系的に活用できます。
関連する項を(物理的性質、代数的関係、問題固有の対称性のいずれによってであれ)グループ化することで、いくつかの利点が得られます。
-
最適化された回路合成: グループ化された項は、問題固有の情報をスタック全体に伝え、合成された回路の最適化をより簡単にします。その結果、回路の深さとゲート数が減少します。
-
物理構造の保存: グループは、元の問題の意味のある構造(たとえば相互作用パターンや対称性)を符号化し、物理的により意味のある分解に利用できます。
-
フローセットの選択: グループ化は、[1] に示されているように、フローセットや、量子シミュレーションに関連するその他のグラフ理論的構造を自然に符号化できます。
-
アルゴリズムの安定性の向上: SqDRIFT ルーチンで示されているように、一部のアルゴリズムは、物理的性質が保存されることで安定性や性能が向上する場合があります。
-
積公式の因子: フェルミオン積公式はオペレーターをそのグループに分割するため、グループ化が時間発展のコストとトロッター誤差の両方を決定します。フェルミオン空間での Evolution ゲートの合成を参照してください。
使い方
オペレーターのグループ化を定義する最も簡単な方法は、対象オペレーターの 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>
これは、有向エッジで接続されたマヨラナ・モードの単純な2×3の格子です。各エッジは、接続された2つのモードに作用するオペレーター内の項を表します。目標は、これらの項をエッジのラベル(グラフ上に表示)に従ってグループ化することです。同じラベルのエッジで接続された項は、同じグループに入れられます。次のコードは、その方法を示しています。
- 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);
項ごとに1つのグループ・インデックスが強制される
グループ配列は項ごとに1つのインデックスを保持し、長さの異なる配列を代入するとエラーが送出されます。下流ではこの不変条件が再検査されないため、インデックスがオペレーターに入る時点で強制されます。短すぎる配列は末尾の項を黙って落とし、長すぎる配列はどの項も持たないグループを作り出してしまうためです。
>>> 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 を代入してグループ・インデックスをクリアすることは常に許可されています。
2つの検査は独立している
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.