多次元配列の合計関数

Hi-QUBOは,変数や式の多次元配列に対する2つの合計関数を提供しています:

  • qbpp::sum(): 配列の全要素の合計を計算します.

  • qbpp::vector_sum(): 最も内側(最低次元)の次元に沿った合計を計算します. 結果の配列は,入力配列より1つ少ない次元を持ちます. 入力配列の次元は2以上である必要があります.

以下のプログラムは,qbpp::sum() と qbpp::vector_sum() の違いを示しています:

sum-functions-for-arrays-program1.cpp
#include <qbpp/qbpp.hpp>

int main() {
  auto x = qbpp::var("x", 2, 3, 3);
  auto y = x + 1;
  for (size_t i = 0; i < 2; ++i) {
    for (size_t j = 0; j < 3; ++j) {
      for (size_t k = 0; k < 3; ++k) {
        std::cout << "y[" << i << "][" << j << "][" << k << "] = " << y[i][j][k]
                  << std::endl;
      }
    }
  }
  auto sum = qbpp::sum(y).simplify();
  std::cout << "sum(y) = " << sum << std::endl;
  auto vector_sum = qbpp::vector_sum(y).simplify();
  for (size_t i = 0; i < 2; ++i) {
    for (size_t j = 0; j < 3; ++j) {
      std::cout << "vector_sum[" << i << "][" << j << "] = " << vector_sum[i][j]
                << std::endl;
    }
  }
}

まず,サイズ \(2 \times 3 \times 3\) の変数配列 x を定義します. 次に,x の全要素に1を加えた配列 y を作成し,y の全要素を出力します. 続いて,qbpp::sum(y) を計算して出力します. その後,qbpp::vector_sum() 関数を y に適用し,結果をサイズ \(2 \times 3\) の2次元式配列 vector_sum に格納します. 最後に,vector_sum の全要素を出力します.

このプログラムは以下の出力を生成します:

y[0][0][0] = 1 +x[0][0][0]
y[0][0][1] = 1 +x[0][0][1]
y[0][0][2] = 1 +x[0][0][2]
y[0][1][0] = 1 +x[0][1][0]
y[0][1][1] = 1 +x[0][1][1]
y[0][1][2] = 1 +x[0][1][2]
y[0][2][0] = 1 +x[0][2][0]
y[0][2][1] = 1 +x[0][2][1]
y[0][2][2] = 1 +x[0][2][2]
y[1][0][0] = 1 +x[1][0][0]
y[1][0][1] = 1 +x[1][0][1]
y[1][0][2] = 1 +x[1][0][2]
y[1][1][0] = 1 +x[1][1][0]
y[1][1][1] = 1 +x[1][1][1]
y[1][1][2] = 1 +x[1][1][2]
y[1][2][0] = 1 +x[1][2][0]
y[1][2][1] = 1 +x[1][2][1]
y[1][2][2] = 1 +x[1][2][2]
sum(y) = 18 +x[0][0][0] +x[0][0][1] +x[0][0][2] +x[0][1][0] +x[0][1][1] +x[0][1][2] +x[0][2][0] +x[0][2][1] +x[0][2][2] +x[1][0][0] +x[1][0][1] +x[1][0][2] +x[1][1][0] +x[1][1][1] +x[1][1][2] +x[1][2][0] +x[1][2][1] +x[1][2][2]
vector_sum[0][0] = 3 +x[0][0][0] +x[0][0][1] +x[0][0][2]
vector_sum[0][1] = 3 +x[0][1][0] +x[0][1][1] +x[0][1][2]
vector_sum[0][2] = 3 +x[0][2][0] +x[0][2][1] +x[0][2][2]
vector_sum[1][0] = 3 +x[1][0][0] +x[1][0][1] +x[1][0][2]
vector_sum[1][1] = 3 +x[1][1][0] +x[1][1][1] +x[1][1][2]
vector_sum[1][2] = 3 +x[1][2][0] +x[1][2][1] +x[1][2][2]

同じ結果は明示的なforループでも得ることができます. ただし,大きな配列の場合は qbpp::sum() と qbpp::vector_sum() を使用することを推奨します.これらの関数は(式の配列の sum() と vector_sum() について)大きな配列では内部でマルチスレッドを活用して計算を高速化します.

vector_sum() の軸指定

デフォルトでは,qbpp::vector_sum() は最も内側(最後)の軸に沿って合計を計算します. qbpp::vector_sum(array, axis) で異なる軸を指定できます. 負のインデックスもサポートされています: 軸 -1 は最後の軸,-2 は最後から2番目の軸を指します.

上記と同じ \(2 \times 3 \times 3\) の配列 x を使って,3つの軸それぞれに沿った合計を示します:

  auto vs2 = qbpp::vector_sum(x, 2).simplify();  // 軸2に沿って合計(デフォルト)
  auto vs1 = qbpp::vector_sum(x, 1).simplify();  // 軸1に沿って合計
  auto vs0 = qbpp::vector_sum(x, 0).simplify();  // 軸0に沿って合計
  • qbpp::vector_sum(x, 2) は軸2(最も内側の軸)に沿って合計し,\(2 \times 3\) の配列を生成します.これは qbpp::vector_sum(x) と同等です.

vs2[0][0] = x[0][0][0] +x[0][0][1] +x[0][0][2]
vs2[0][1] = x[0][1][0] +x[0][1][1] +x[0][1][2]
vs2[0][2] = x[0][2][0] +x[0][2][1] +x[0][2][2]
vs2[1][0] = x[1][0][0] +x[1][0][1] +x[1][0][2]
vs2[1][1] = x[1][1][0] +x[1][1][1] +x[1][1][2]
vs2[1][2] = x[1][2][0] +x[1][2][1] +x[1][2][2]
  • qbpp::vector_sum(x, 1) は軸1(中間の軸)に沿って合計し,\(2 \times 3\) の配列を生成します.

vs1[0][0] = x[0][0][0] +x[0][1][0] +x[0][2][0]
vs1[0][1] = x[0][0][1] +x[0][1][1] +x[0][2][1]
vs1[0][2] = x[0][0][2] +x[0][1][2] +x[0][2][2]
vs1[1][0] = x[1][0][0] +x[1][1][0] +x[1][2][0]
vs1[1][1] = x[1][0][1] +x[1][1][1] +x[1][2][1]
vs1[1][2] = x[1][0][2] +x[1][1][2] +x[1][2][2]
  • qbpp::vector_sum(x, 0) は軸0(最も外側の軸)に沿って合計し,\(3 \times 3\) の配列を生成します.

vs0[0][0] = x[0][0][0] +x[1][0][0]
vs0[0][1] = x[0][0][1] +x[1][0][1]
vs0[0][2] = x[0][0][2] +x[1][0][2]
vs0[1][0] = x[0][1][0] +x[1][1][0]
vs0[1][1] = x[0][1][1] +x[1][1][1]
vs0[1][2] = x[0][1][2] +x[1][1][2]
vs0[2][0] = x[0][2][0] +x[1][2][0]
vs0[2][1] = x[0][2][1] +x[1][2][1]
vs0[2][2] = x[0][2][2] +x[1][2][2]

PyQBPPは,変数や式の多次元配列に対する2つの総和関数を提供しています:

  • sum(): 配列のすべての要素の総和を計算します.

  • vector_sum(): 最も内側の次元に沿って総和を計算します.入力配列の次元は2以上である必要があります(1次元配列は ValueError). 結果の配列は入力配列より1次元少なくなります.

以下のプログラムは sum() と vector_sum() の違いを示しています:

sum-functions-for-arrays-program1.py
import pyqbpp as qbpp

x = qbpp.var("x", shape=(2, 3, 3))
y = x + 1
for i in range(2):
    for j in range(3):
        for k in range(3):
            print(f"y[{i}][{j}][{k}] =", y[i][j][k])

s = qbpp.sum(y)
s.simplify()
print("qbpp.sum(y) =", s)

vs = qbpp.vector_sum(y)
for i in range(2):
    for j in range(3):
        print(f"vector_sum[{i}][{j}] =", vs[i][j])

まず,サイズ \(2 \times 3 \times 3\) の変数配列 x を定義します. 次に,x のすべての要素に1を加えて配列 y を作成します. そして,sum(y) で全18要素の総和を計算します. その後,vector_sum(y) で最も内側の次元に沿って総和を計算し,\(2 \times 3\) の配列を生成します.

このプログラムの出力は以下の通りです:

y[0][0][0] = 1 +x[0][0][0]
y[0][0][1] = 1 +x[0][0][1]
y[0][0][2] = 1 +x[0][0][2]
y[0][1][0] = 1 +x[0][1][0]
y[0][1][1] = 1 +x[0][1][1]
y[0][1][2] = 1 +x[0][1][2]
y[0][2][0] = 1 +x[0][2][0]
y[0][2][1] = 1 +x[0][2][1]
y[0][2][2] = 1 +x[0][2][2]
y[1][0][0] = 1 +x[1][0][0]
y[1][0][1] = 1 +x[1][0][1]
y[1][0][2] = 1 +x[1][0][2]
y[1][1][0] = 1 +x[1][1][0]
y[1][1][1] = 1 +x[1][1][1]
y[1][1][2] = 1 +x[1][1][2]
y[1][2][0] = 1 +x[1][2][0]
y[1][2][1] = 1 +x[1][2][1]
y[1][2][2] = 1 +x[1][2][2]
qbpp.sum(y) = 18 +x[0][0][0] +x[0][0][1] +x[0][0][2] +x[0][1][0] +x[0][1][1] +x[0][1][2] +x[0][2][0] +x[0][2][1] +x[0][2][2] +x[1][0][0] +x[1][0][1] +x[1][0][2] +x[1][1][0] +x[1][1][1] +x[1][1][2] +x[1][2][0] +x[1][2][1] +x[1][2][2]
vector_sum[0][0] = 3 +x[0][0][0] +x[0][0][1] +x[0][0][2]
vector_sum[0][1] = 3 +x[0][1][0] +x[0][1][1] +x[0][1][2]
vector_sum[0][2] = 3 +x[0][2][0] +x[0][2][1] +x[0][2][2]
vector_sum[1][0] = 3 +x[1][0][0] +x[1][0][1] +x[1][0][2]
vector_sum[1][1] = 3 +x[1][1][0] +x[1][1][1] +x[1][1][2]
vector_sum[1][2] = 3 +x[1][2][0] +x[1][2][1] +x[1][2][2]

明示的なforループを使っても同じ結果が得られます. しかし,大きな配列では sum() と vector_sum() の使用を推奨します.これらの関数は(式の配列の sum() と vector_sum() について)大きな配列では内部的にマルチスレッドを活用して計算を高速化するためです.

受け付ける入力

qbpp.sum() は qbpp 配列だけでなく,list, tuple, ジェネレータ式, range など任意の Python iterable を受け付けます. 配列以外の入力は内部で qbpp.array(...) に暗黙変換されてから同じ高速パスで合計されるため,戻り値は常にスカラの Expr です.

# qbpp 配列(多次元)— 全要素の総和
qbpp.sum(x)

# 疎なインデックス集合に対する内包表記(例: グラフのエッジ)
qbpp.sum([~x[u] * ~x[v] for u, v in edges])

# ジェネレータ式 — 上と等価でやや軽量
qbpp.sum(~x[u] * ~x[v] for u, v in edges)

# 整数の iterable も動作する
qbpp.sum(range(10))   # → 45

この機能は,配列演算では表現しにくい疎で不規則な総和(グラフのエッジ集合,集合への所属など)を書くときに特に便利です.

注意: Python 標準の sum() も qbpp 配列に対して動作しますが,多次元では挙動が異なります.2次元配列 y に対して sum(y) は軸0で縮約して1次元配列を返しますが,qbpp.sum(y) は全要素の総和(スカラ)を返します(numpy.sum と同じ規約).QUBO の定式化では常に qbpp.sum() を使ってください.

vector_sum() の軸指定

デフォルトでは,vector_sum() は最も内側(最後)の軸に沿って合計を計算します. vector_sum(array, axis) で異なる軸を指定できます. 負のインデックスもサポートされています: 軸 -1 は最後の軸,-2 は最後から2番目の軸を指します.

上記と同じ \(2 \times 3 \times 3\) の配列 x を使って,3つの軸それぞれに沿った合計を示します:

vs2 = qbpp.vector_sum(x, axis=2)  # 軸2に沿って合計(デフォルト)
vs1 = qbpp.vector_sum(x, axis=1)  # 軸1に沿って合計
vs0 = qbpp.vector_sum(x, axis=0)  # 軸0に沿って合計
  • vector_sum(x, axis=2) は軸2(最も内側の軸)に沿って合計し,\(2 \times 3\) の配列を生成します.これは vector_sum(x) と同等です.

vs2[0][0] = x[0][0][0] +x[0][0][1] +x[0][0][2]
vs2[0][1] = x[0][1][0] +x[0][1][1] +x[0][1][2]
vs2[0][2] = x[0][2][0] +x[0][2][1] +x[0][2][2]
vs2[1][0] = x[1][0][0] +x[1][0][1] +x[1][0][2]
vs2[1][1] = x[1][1][0] +x[1][1][1] +x[1][1][2]
vs2[1][2] = x[1][2][0] +x[1][2][1] +x[1][2][2]
  • vector_sum(x, axis=1) は軸1(中間の軸)に沿って合計し,\(2 \times 3\) の配列を生成します.

vs1[0][0] = x[0][0][0] +x[0][1][0] +x[0][2][0]
vs1[0][1] = x[0][0][1] +x[0][1][1] +x[0][2][1]
vs1[0][2] = x[0][0][2] +x[0][1][2] +x[0][2][2]
vs1[1][0] = x[1][0][0] +x[1][1][0] +x[1][2][0]
vs1[1][1] = x[1][0][1] +x[1][1][1] +x[1][2][1]
vs1[1][2] = x[1][0][2] +x[1][1][2] +x[1][2][2]
  • vector_sum(x, axis=0) は軸0(最も外側の軸)に沿って合計し,\(3 \times 3\) の配列を生成します.

vs0[0][0] = x[0][0][0] +x[1][0][0]
vs0[0][1] = x[0][0][1] +x[1][0][1]
vs0[0][2] = x[0][0][2] +x[1][0][2]
vs0[1][0] = x[0][1][0] +x[1][1][0]
vs0[1][1] = x[0][1][1] +x[1][1][1]
vs0[1][2] = x[0][1][2] +x[1][1][2]
vs0[2][0] = x[0][2][0] +x[1][2][0]
vs0[2][1] = x[0][2][1] +x[1][2][1]
vs0[2][2] = x[0][2][2] +x[1][2][2]