# Einsum: numpy 風のテンソル縮約 :::{container} prog-cpp QUBO++ は `qbpp::einsum(subscript, arrays...)` を提供します。これは NumPy のアインシュタイン縮約と同じ記法で、整数・変数・項・式の多次元配列を1行で縮約できる関数です。 テンプレート引数 `OutDim` は出力配列の次元数を指定し、`subscript` の出力ラベル数と実行時に照合されます。 ## C++ でなぜ OutDim が必要か QUBO++ の多次元配列は `Array` という型で表現されます。ここで次元 `Dim` はテンプレート引数(コンパイル時定数)であり、`Array<1, Expr>` と `Array<2, Expr>` はまったく別の型になります。 `einsum` のコード生成時点で、コンパイラは戻り値の型を確定する必要があります。 しかし出力次元は `subscript` 文字列(例: `"ij,jk->ik"` なら2次元、`"i,i->"` ならスカラー)から決まります。`subscript` は `const char*` 引数として実行時にしか解析できないため、C++ コンパイラは出力次元を推論できません。 そのため、呼び出し側がテンプレート引数として明示的に `OutDim` を指定する必要があります。 ```{include} ../../programFiles/markDown/advanced/Einstein-sum-program.md :start-after: :end-before: ``` `OutDim` と `subscript` の実際の出力ラベル数が一致しているかは実行時に検査され、不一致の場合はエラーで終了します。誤ったテンプレート引数が「形の違う配列」として静かに通ってしまうことはありません。 ## Python 版で OutDim が不要な理由 Python ではオブジェクトが次元情報を実行時に保持しているためです。Python バインディング側で `subscript` を解析し、正しい次元の出力配列を自動的に構築しています。 ::: :::{container} prog-python PyQBPP は `qbpp.einsum(subscript, *arrays)` を提供します。 これは numpy の アインシュタイン縮約 と同じ記法で、整数・変数・項・式の多次元配列を 1 行で縮約できる関数です。 出力配列の次元は subscript から自動的に推論されます。 C++ 版の `qbpp::einsum(...)` のようにテンプレート引数で次元を 指定する必要はなく、Python 版は subscript と入力配列のみを渡します。 ::: ## subscript の文法 ``` "labels1,labels2,...->out_labels" ``` - 各 **label** は ASCII 1 文字(`,`・`-`・`>`・空白を除く)です。 - 各入力配列は、その次元数とちょうど同じ数のラベルを持つ必要があります。 - 入力に現れて出力に現れないラベルは **縮約(総和)** されます。 - 入力と出力の両方に現れるラベルは **自由軸として保持** されます。 - **同一入力内に同じラベルが2回現れる場合**、その2つの軸は結合されます(trace や対角抽出に使用します)。 - 暗黙形式 `"ij,jk"`(`->` を省略)では、全入力中にちょうど1回だけ現れるラベルをアルファベット順に並べたものが出力になります(NumPy と同様の仕様)。 - 右辺が空(`"i,i->"`)の場合は**スカラー出力**(`OutDim == 0`)になります。 ## 出力型 - すべての入力が整数配列(`Array`)の場合、結果も整数配列 `Array` になります。`OutDim == 0` のときは `coeff_t` のスカラーが返ります。 - それ以外(`Var`、`Term`、`Expr` のいずれかを1つでも含む場合)は `Array` が返ります。`OutDim == 0` のときは `Expr` のスカラーが返ります。 --- ## 使用例 以下のプログラムは `einsum` の代表的な使い方を示します。 :::{container} prog-cpp ```{literalinclude} ../../programFiles/cppPrograms/advanced/Einstein-sum-program1.cpp :language: cpp :caption: Einstein-sum-program1.cpp ``` ::: :::{container} prog-python ```{literalinclude} ../../programFiles/pythonPrograms/advanced/Einstein-sum-program1.py :language: python :caption: Einstein-sum-program1.py ``` ::: このプログラムは以下を出力します: ```{include} ../../programFiles/markDown/advanced/Einstein-sum-program.md :start-after: :end-before: ``` ## 3 つ以上の入力 einsum は任意個数の入力配列を受け取れます。組合せ最適化での代表例は 二次割当問題(QAP) 形の目的関数 $\displaystyle \sum_{i,j,k,l} f_i d_{kl} x_{ik} x_{jl}$ です: :::{container} prog-cpp ```{include} ../../programFiles/markDown/advanced/Einstein-sum-program.md :start-after: :end-before: ``` ::: :::{container} prog-python ```{include} ../../programFiles/markDown/advanced/Einstein-sum-program.md :start-after: :end-before: ``` ::: ## どのような場面で使うか 目的関数や制約が「テンソル添字でインデックスされた積の総和」として表現できる場合、`einsum` を使うのが最も簡潔です。 明示的な多重ループと比較して、以下の利点があります。 - 数式構造をそのまま直接表現できます。 - 添字計算のミスを避けられます。 - 大規模配列では内部でマルチスレッド化され、高速に実行されます。 単純な全要素総和や軸ごとの総和には、`qbpp.sum()` や `qbpp.vector_sum()`(「Sum 関数」を参照)の方がより直接的です。 一方で、複数配列の積を扱う場合や、インデックス間の関係が複雑になった場合には、`einsum` の利用を検討してください。