多次元配列の総和関数
PyQBPPは、変数や式の多次元配列に対する2つの総和関数を提供しています:
sum(): 配列のすべての要素の総和を計算します。vector_sum(): 最も内側の次元に沿って総和を計算します。入力配列の次元は2以上である必要があります(1次元配列はValueError)。 結果の配列は入力配列より1次元少なくなります。
以下のプログラムは sum() と vector_sum() の違いを示しています:
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
この機能は、配列演算では表現しにくい疎で不規則な総和(グラフのエッジ集合、集合への所属など)を書くときに特に便利です。
組み込みの sum() ではなく qbpp.sum() を使う
式の総和には、Python 組み込みの sum() ではなく、必ず qbpp.sum() を使ってください。
import pyqbpp as qbpp
n = 1000
x = qbpp.var("x", n)
edges = [(i, (i + 1) % n) for i in range(n)]
# ❌ 組み込みの sum(): 項数が多いと遅くなることがある
f = sum(~x[u] * ~x[v] for u, v in edges)
# ✅ qbpp.sum(): 同じ式を速く作る
g = qbpp.sum(~x[u] * ~x[v] for u, v in edges)
理由は 2 つあります。
- 速度: 組み込みの
sum()は要素を+で 1 つずつ足すので、f = f + xをループで繰り返すのと同じになり、実行環境によっては時間が項数の 2 乗に比例して増えます。例えば 4 万項の和に数秒かかることがあり(qbpp.sum()は 0.2 秒以下)、項数が 10 倍になると時間は約 100 倍になります。qbpp.sum()は共有ライブラリの中でまとめて足すので、どの環境でも項数に比例する時間で済みます。 - 多次元配列の意味: 2 次元配列
yに対して組み込みのsum(y)は軸 0 で縮約して 1 次元配列を返しますが、qbpp.sum(y)は全要素の総和(スカラー)を返します(numpy.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]