演算子表現の設計原則
このガイドでは、operators モジュール内のすべての演算子表現に共通する設計原則と中核的な概念を説明します。
概要
このモジュールが提供する演算子表現は、いくつかの基本的な設計原則を共有しています。
-
疎なデータ構造: 演算子は、恒等でない操作のみをエンコードします。内部のデータレイアウトは、おおむね疎行列のデータ形式に着想を得ており、モード数が多くても有意な寄与が比較的少ない系に対して、効率的な保存と計算を可能にします。
-
項の反復処理と再構築: 内部の疎な格納方式に関わらず、演算子は一貫した反復処理のインターフェースを提供するため、基になるデータ構造を理解しなくても、項を調べ、フィルタリングし、変換し、変更した項から新しい演算子を再構築できます。
-
モードに基づくインデックス付け: 演算子は、フェルミオンの自由度を区別するために抽象的なモードインデックスを使い、物理系から演算子表現への柔軟なマッピングを可能にします。
-
項のグループ化と交換関係: 演算子は、項をグループインデックスに関連付けるグループ化情報を標準でサポートしています。これにより、別個のデータ構造を必要とせずに、最適化と物理的な構造の保存が可能になります。実際の使い方については、グループ化ガイドを参照してください。
-
算術演算と数学的演算: すべての演算子は、
OperatorTraitプロトコルを使って、一貫した算術演算(加算、乗算、合成など)と数学関数のセットを実装しており、異なる演算子タイプ間で統一的なコードを書けます。 -
演算子の項の順序と標準形: 数学的に等価な演算子でも、量子アルゴリズムでの表現や挙動は大きく異なる場合があります。すべての演算子表現は、(代数に固有の交換関係に基づく)さまざまな標準形をサポートしており、正準で予測可能かつ最適化しやすい演算子表現を実現します。
疎なデータ構造
すべての演算子は、恒等でない操作のみを追跡する疎な表現を使います。恒等でない各操作は、係数(複素数)と、特定のモードに対するアクションの列で構成されます。
このアプローチにより、特にモード数が多くても有意な寄与が比較的少ない系で、メモリ使用量と計算時間が大幅に削減されます。恒等でない操作のみをエンコードすることで、演算は重要な部分だけに集中し、密な表現では実行不可能な大規模な系も扱えるようになります。さらに、演算子は任意のモード数に自然にスケールします。モード {0, 1} に作用する演算子は、影響を受けないモードが暗黙的に恒等であるため、はるかに多くのモードをもつ系でも、そのまま動作します。
同一の項は算術演算の間も別々に保持され、必要な場合は明示的に結合する必要があります。
内部の格納形式
内部的には、演算子は疎行列のデータ形式に着想を得た配列に格納されます。
-
係数配列: 各項の複素係数
-
モードインデックス配列: 各アクションが作用するフェルミオンモード
-
境界配列: モード配列内で、各項のモードが始まる位置と終わる位置を示すインデックス
演算子タイプによっては、追加の配列が存在する場合があります。たとえば、FermionOperator のインスタンスには、対応するモードインデックスに作用するフェルミオンアクションの種類を指定する、ブール値のアクション配列が含まれます。対照的に、MajoranaOperator クラスは、その情報をモードインデックスのパリティにエンコードしているため、この区別を必要としません。完全な格納形式については、お使いの演算子タイプのAPIドキュメントを参照してください。
以下の例では、これらの配列がどのように構成されているかを示します。最初は、疎な配列を使った直接的な構築です。
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator
>>>
>>> # Construct operators directly using sparse arrays
>>> # First operator: 1.0 * +0 -1
>>> op1 = FermionOperator(
... coeffs=[1.0],
... actions=[True, False],
... modes=[0, 1],
... boundaries=[0, 2],
... )
>>>
>>> # Second operator: 1.0 * +2 -3
>>> op2 = FermionOperator(
... coeffs=[1.0],
... actions=[True, False],
... modes=[2, 3],
... boundaries=[0, 2],
... )
>>>
>>> # Combine the sparse operators
>>> op1 += op2
>>> print(format(op1))
1.000000e0 +0.000000e0j * (+0 -1)
1.000000e0 +0.000000e0j * (+2 -3)
#include <qiskit_fermions.h>
// Construct first operator: 1.0 * +0 -1
QkComplex64 coeff1[1] = {{1.0, 0.0}};
bool actions1[2] = {true, false};
uint32_t modes1[2] = {0, 1};
uint32_t boundaries1[2] = {0, 2};
QfFermionOperator *op1 = qf_ferm_op_new(1, 2, coeff1, actions1, modes1, boundaries1);
// Construct second operator: 1.0 * +2 -3
QkComplex64 coeff2[1] = {{1.0, 0.0}};
bool actions2[2] = {true, false};
uint32_t modes2[2] = {2, 3};
uint32_t boundaries2[2] = {0, 2};
QfFermionOperator *op2 = qf_ferm_op_new(1, 2, coeff2, actions2, modes2, boundaries2);
// Combine the sparse operators. There is no in-place add in the C API, so
// the sum is a new operator that the caller owns.
QfFermionOperator *sum = qf_ferm_op_add(op1, op2);
qf_ferm_op_free(op1);
qf_ferm_op_free(op2);
qf_ferm_op_free(sum);
便利な構築メソッド
Python開発者向けに、疎な格納形式の詳細を抽象化する便利な構築メソッドがいくつか用意されています。これらを使うと、係数、モード、境界の配列を管理することなく演算子を簡単に構築できます。
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Construct operators using operator algebra notation
>>> op1 = FermionOperator.from_dict({(cre(0), ann(1)): 1.0})
>>> op2 = FermionOperator.from_dict({(cre(2), ann(3)): 1.0})
>>>
>>> # The result is sparse even when combining them
>>> op1 += op2
>>> print(format(op1))
1.000000e0 +0.000000e0j * (+0 -1)
1.000000e0 +0.000000e0j * (+2 -3)
// The C API uses direct array construction; convenience methods are not available.
個々の演算子の実装では、その用途に合わせた追加の構築メソッドがサポートされている場合があります。利用可能なすべての構築オプションについては、お使いの演算子タイプのAPIドキュメントを確認してください。
項の反復と再構築
演算子は、内部の疎な表現にかかわらず、OperatorTrait.iter_terms()を使って一貫した反復インターフェースを提供します。そのため、基盤となるデータ構造を理解しなくても、項の検査、フィルタリング、変換ができます。変換した項から、OperatorTrait.from_terms()を使って新しい演算子を再構築できます。
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Construct an operator with terms of different orders
>>> op = FermionOperator.from_dict({
... (): 0.5, # constant term (order 0)
... (cre(0), ann(1)): 1.0, # two-body term (order 2)
... (cre(0), cre(1), ann(1), ann(0)): 0.25 # four-body term (order 4)
... })
>>>
>>> # Filter to keep only terms of order 2
>>> order_two_terms = [
... (term, coeff) for term, coeff in op.iter_terms()
... if len(term) == 2
... ]
>>>
>>> # Reconstruct operator from filtered terms
>>> filtered_op = FermionOperator.from_terms(order_two_terms)
>>> print(f"Original operator has {len(op)} terms")
Original operator has 3 terms
>>> print(f"Filtered operator has {len(filtered_op)} term")
Filtered operator has 1 term
// WARNING: Term iteration and filtering are not yet available in the C API.
モードベースのインデックス付け
すべての演算子表現は、項が作用するインデックスをモードと呼びます。モードとは、システム内のフェルミオンの自由度を識別する単なるインデックスです。物理的な自由度(空間軌道、スピン状態、その他の量子数など)からモードインデックスへの対応付けはユーザーに任されており、最大限の柔軟性が得られます。
この抽象化はqiskit_fermions.circuitモジュールにも存在し、FermionicCircuitはフェルミオンモードのレジスターに対して作用します。演算子表現と回路表現のどちらでも、モードは、ある操作にどの自由度が関与するかを、代数に依存しない一貫した方法で指定します。
現在の実装はスピンなしのモードを使用します: このモジュールが現在提供するすべての演算子表現は、モードをスピンなしのフェルミオン自由度として扱います。つまり、システムにスピンアップとスピンダウンの電子またはフェルミオンの両方がある場合は、それらを明示的に別々のモードに対応付ける必要があります(たとえば、4つの空間軌道のスピンアップにモード0〜3、スピンダウンにモード4〜7を割り当てる、または任意の他の規則を選ぶなど)。
この設計により、コア表現をシンプルかつ汎用的に保ち、特定のスピン順序の規則を押し付けずに済みます。qiskit_fermions.operators.libraryのようなユーティリティモジュールは、電子構造データを読み込む際にこうした対応付けを代行する便利な関数(たとえばFCIDump.from_file())を提供します。
パッケージの進化に伴い、スピンの自由度をデータモデルでネイティブにサポートする、スピンありの演算子表現が追加される可能性があります。これらは現在のスピンなしの実装と明確に区別され、モジュール内で共存します。
項のグループ化と交換関係
係数やモードインデックスと同様に、演算子は各項をグループインデックスに関連付けるグループ配列を任意で保持できます。グループ化を疎なデータ構造の一部として演算子表現に直接組み込むことで、グループ化の情報は変換を通じて自然に演算子とともに引き継がれます。これにより、物理的な性質、代数的な関係、問題固有の対称性のいずれに由来する構造であっても、体系的に利用できます。この構造化された情報は、OperatorTrait.split_out_groups()のようなメソッドを使って、回路合成や分解といった後続の操作で活用できます。
ワークフローで演算子の項をグループ化する方法の詳細については、グループ化ガイドを参照してください。
- PYTHON
- C
>>> from qiskit_fermions.operators import MajoranaOperator, gamma
>>> op = MajoranaOperator.from_dict({
... (gamma(0, False),): 1.0,
... (gamma(1, False),): 1.0,
... (gamma(2, False), gamma(3, False)): 1.0
... })
>>> # Assign group indices to terms
>>> op.groups = [0, 0, 1]
>>> # Partition operator by groups
>>> grouped_ops = op.split_out_groups()
#include <qiskit_fermions.h>
// Create operator with 3 terms
QkComplex64 coeffs[3] = {{1.0, 0.0}, {1.0, 0.0}, {1.0, 0.0}};
uint32_t modes[4] = {0, 1, 2, 3};
uint32_t boundaries[4] = {0, 1, 2, 4};
QfMajoranaOperator *op = qf_maj_op_new(3, 4, coeffs, modes, boundaries);
// Assign grouping information
uint32_t groups[3] = {0, 0, 1};
qf_maj_op_set_groups(op, groups, 3);
// Partition operator by groups
QfMajoranaOperator *grouped_ops[2];
qf_maj_op_split_out_groups(op, NULL, 0, grouped_ops);
算術演算と数学的演算
すべての演算子はOperatorTraitプロトコルを実装しており、異なる演算子タイプ間で統一された一連の操作を提供します。これにより、ある演算子表現向けに書いたコードが他の表現でも同様に動作します。このプロトコルが、このパッケージで定義されている他のプロトコルとどのように関係するかについては、qiskit_fermions.protocolsを参照してください。
このプロトコルには、算術演算(加算、乗算、合成など)、構造的な操作(項の反復、モードサポートの解析、ラベルの付け替え)、数学的な関数(正規順序化、簡約化、等価性チェック)などが含まれます。利用可能なすべての操作の完全なリファレンスについては、OperatorTraitのドキュメントを参照してください。
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Construct a Hermitian operator: H = +0 -1 + +1 -0
>>> op = FermionOperator.from_dict({
... (cre(0), ann(1)): 1.0,
... (cre(1), ann(0)): 1.0
... })
>>>
>>> # Check if the operator is Hermitian
>>> print(f"Operator is Hermitian: {op.is_hermitian(atol=1e-10)}")
Operator is Hermitian: True
#include <qiskit_fermions.h>
// Construct a Hermitian operator: H = +0 -1 + +1 -0
QkComplex64 coeffs[2] = {{1.0, 0.0}, {1.0, 0.0}};
bool actions[4] = {true, false, true, false};
uint32_t modes[4] = {0, 1, 1, 0};
uint32_t boundaries[3] = {0, 2, 4};
QfFermionOperator *op = qf_ferm_op_new(2, 4, coeffs, actions, modes, boundaries);
// Check if the operator is Hermitian
bool is_hermitian = qf_ferm_op_is_hermitian(op, 1e-10);
printf("Operator is Hermitian: %s\n", is_hermitian ? "true" : "false");
// Clean up
qf_ferm_op_free(op);
is_hermitian()は、であることを、その差を正規形に簡約してzero()と比較することで検証します。他のプロトコル操作で書き下すと、次の処理を行います。
>>> difference = (op - op.adjoint()).normal_ordered().simplify(atol=1e-10)
>>> difference.equiv(FermionOperator.zero(), atol=1e-10)
True
このチェックの強度は、演算子タイプによって異なります。これは、そのタイプの正規形が隣接する生成子をどれだけ完全に縮約するかに依存するためです。Trueという結果は常に信頼できますが、タイプによってはFalseという結果は保守的な判定です。詳細についてはis_hermitian()を参照してください。
この例ではatol=1e-10を使用しています。atol(絶対許容誤差)パラメーターはしきい値を指定します。大きさがatolより小さい係数はゼロとして扱われ、破棄されます。浮動小数点演算では小さな丸め誤差が生じ、等価な演算子が等価と認識されなくなる可能性があるため、演算子を比較する際の数値的安定性にこれは不可欠です。
OperatorTraitプロトコルは共通のインターフェースを提供しますが、個々の演算子の実装には、プロトコルに含まれない追加の便利なメソッドがある場合があります。たとえば、FermionOperatorとMajoranaOperatorは、最長の項の長さを返すmax_rank()メソッドを提供します。利用可能なすべての機能を確認するには、必ずお使いの演算子タイプのAPIドキュメントを参照してください。
演算子の項の順序と正規形
量子演算子の代数における根本的な課題は、数学的に等価な演算子がさまざまな方法で表現でき、それぞれが量子アルゴリズムに異なる影響を与えることです。同じ演算子を代数的に等価な形式で書くことができますが(たとえば、はと表せます)、これらの表現は回路合成、簡約化、数値アルゴリズムにおいて異なる挙動をもたらします。
演算子表現は、交換関係を用いて演算子を代数固有の標準形に変換する正規順序化操作をサポートしています。これにより、信頼性の高い比較(等価な2つの演算子は同一の正規順序形を持ちます)、簡約化の発見(交換関係により項が相殺または結合されます)、そして正しさと効率のために特定の演算子形式を必要とするアルゴリズムのサポートが可能になります。
演算子表現によっては、それぞれの代数に適した異なる交換関係に基づいて正規順序化を実装している場合があります。たとえば、フェルミオンの正規順序化は反交換関係()を使用しますが、マヨラナの正規順序化は異なる代数の規約()を使用します。正規順序化がどのように実装されているかを理解するには、必ずお使いの演算子タイプのドキュメントを参照してください。
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Two different representations of the same operator
>>> op1 = FermionOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> op2 = FermionOperator.from_dict({(): 1.0, (cre(0), ann(0)): -1.0})
>>>
>>> # Direct comparison fails due to different forms
>>> op1.equiv(op2, atol=1e-10)
False
>>>
>>> # Normal-order both and compare again
>>> op1_normal = op1.normal_ordered()
>>> op2_normal = op2.normal_ordered()
>>> op1_normal.equiv(op2_normal, atol=1e-10)
True
#include <qiskit_fermions.h>
#include <stdbool.h>
// Two different representations of the same operator
QkComplex64 coeff1[1] = {{1.0, 0.0}};
bool actions1[2] = {false, true};
uint32_t modes1[2] = {0, 0};
uint32_t boundaries1[2] = {0, 2};
QfFermionOperator *op1 = qf_ferm_op_new(1, 2, coeff1, actions1, modes1, boundaries1);
QkComplex64 coeff2[2] = {{1.0, 0.0}, {-1.0, 0.0}};
bool actions2[2] = {true, false};
uint32_t modes2[2] = {0, 0};
uint32_t boundaries2[3] = {0, 0, 2};
QfFermionOperator *op2 = qf_ferm_op_new(2, 2, coeff2, actions2, modes2, boundaries2);
// Direct comparison fails due to different forms
bool equiv_before = qf_ferm_op_equiv(op1, op2, 1e-10);
printf("Equivalent before normal ordering: %s\n", equiv_before ? "true" : "false");
// Normal-order both and compare again
QfFermionOperator *op1_normal = qf_ferm_op_normal_ordered(op1, NULL);
QfFermionOperator *op2_normal = qf_ferm_op_normal_ordered(op2, NULL);
bool equiv_after = qf_ferm_op_equiv(op1_normal, op2_normal, 1e-10);
printf("Equivalent after normal ordering: %s\n", equiv_after ? "true" : "false");
// Clean up
qf_ferm_op_free(op1);
qf_ferm_op_free(op2);
qf_ferm_op_free(op1_normal);
qf_ferm_op_free(op2_normal);
OperatorTrait.normal_ordered()プロトコルメソッドは、位置引数とキーワード引数を意図的に未指定のままにしており、具体的な演算子の実装が、生成される正確な標準形を制御する調整可能なパラメーターを定義できるようにしています。これにより、それぞれの代数やユースケースに合わせた、演算子固有の最適化や正規形のバリエーションが可能になります。利用可能なパラメーターについては、お使いの演算子タイプのAPIドキュメントを確認してください。