算術式と条件式#

前章までで、JijModeling における式と型、そして変数の宣言方法について学んできました。 以下では、より複雑な式として、加減乗除などの算術式や、比較式を含む条件式の構築方法について見ていきましょう。

import jijmodeling as jm

算術演算#

Python 組込みの算術演算(+, -, *, /, % などの加減乗除)は、JijModeling の式に対しても用いることができます。 数値型の式同士の演算は期待通り動作するのに加え、(多次元)配列同士や、キー集合が一致する TotalDict に対しても、一定の条件を満たせば演算を行うことができます。 具体的には、以下の組み合わせ(左右問わず)に対して算術演算がサポートされています:

  1. スカラー同士の算術演算

  2. スカラーと多次元配列の算術演算

  3. スカラーと辞書の算術演算

  4. 同じシェイプを持つ多次元配列同士の算術演算

  5. 同じキー集合を持つ全域辞書(TotalDict)同士の算術演算

JijModeling におけるブロードキャスト

(2) と (3) は、NumPy などで見られる、スカラーをコレクションの各要素に適用するブロードキャスト演算に相当します。一方、(4) と (5) は、対応する要素同士の演算です。 NumPy ではより一般のシェイプ間の演算(たとえば \((N, M, L)\)\((M, L)\) の間の演算など)もサポートされています。 このような NumPy の一般化されたブロードキャスト演算は簡潔な略記が可能になる一方で、後ほど読み返す際に意図が不明確になることが多々あります。 このため、JijModeling では意図的にブロードキャストの範囲を制限し、誰にとっても曖昧性がないと思われる場合にのみサポートしています。

言葉だとわかりづらいと思いますので、例を見てみましょう。

problem = jm.Problem("Arithmetic Operations")
x = problem.BinaryVar("x", description="スカラーの決定変数")
N = problem.Length("N")
M = problem.Length("M")
y = problem.IntegerVar(
    "y", lower_bound=0, upper_bound=10, shape=(N, M), description="2次元配列の決定変数"
)
z = problem.ContinuousVar(
    "z",
    lower_bound=-1,
    upper_bound=42,
    shape=(N, M, N),
    description="3次元配列の決定変数",
)
S = problem.TotalDict("S", dtype=float, dict_keys=N, description="スカラーの全域辞書")
s = problem.ContinuousVar("s", lower_bound=0, upper_bound=10, dict_keys=N)
W = problem.Float("w", shape=(N, M))

problem
\[\begin{array}{rl} \text{Problem}\colon &\text{Arithmetic Operations}\\\displaystyle \min &\displaystyle 0\\&\\\text{where}&\\&\text{Decision Variables:}\\&\qquad \begin{alignedat}{2}{s}_{i}&\in \mathbb{R}\;\left(0\leq {s}_{i}\leq 10\right)&\qquad &\text{a dictionary of }\text{continuous}\text{ decision variables}\\&\forall i\in \left\{0,\ldots ,N-1\right\}&&\\&&&\\x&\in \left\{0,1\right\}&\qquad &\text{a binary decision variable}\\&&&\text{スカラーの決定変数}\\&&&\\{y}_{i,j}&\in \mathbb{Z}\;\left(0\leq {y}_{i,j}\leq 10\right)&\qquad &\text{a }2\text{-dim array of }\text{integer}\text{ decision variables}\\&\forall i\in \left\{0,\ldots ,N-1\right\},\;\forall j\in \left\{0,\ldots ,M-1\right\}&&\text{2次元配列の決定変数}\\&&&\\{z}_{i,j,k}&\in \mathbb{R}\;\left(-1\leq {z}_{i,j,k}\leq 42\right)&\qquad &\text{a }3\text{-dim array of }\text{continuous}\text{ decision variables}\\&\forall i\in \left\{0,\ldots ,N-1\right\},\;\forall j\in \left\{0,\ldots ,M-1\right\},\;\forall k\in \left\{0,\ldots ,N-1\right\}&&\text{3次元配列の決定変数}\\\end{alignedat}\\&\\&\text{Placeholders:}\\&\qquad \begin{alignedat}{2}M&\in \mathbb{N}&\qquad &\text{a scalar placeholder in }\mathbb{N}\\&&&\\N&\in \mathbb{N}&\qquad &\text{a scalar placeholder in }\mathbb{N}\\&&&\\{S}_{i}&\in \mathbb{R}&\qquad &\text{a dictionary of placeholders in }\mathbb{R}\\&\forall i\in \left\{0,\ldots ,N-1\right\}&&\text{スカラーの全域辞書}\\&&&\\{w}_{i,j}&\in \mathbb{R}&\qquad &\text{a }2\text{-dim array of placeholders with elements in }\mathbb{R}\\&\forall i\in \left\{0,\ldots ,N-1\right\},\;\forall j\in \left\{0,\ldots ,M-1\right\}&&\\\end{alignedat}\end{array} \]

許容される例#

problem.infer(x + 1)  # OK! (スカラー同士の加算)
\[\mathbb{N}\]
problem.infer(y - x)  # OK! (多次元配列とスカラーの減算)
\[\mathop{\mathrm{Array}}\left[N\times M;\mathbb{Z}\right]\]
problem.infer(S * x)  # OK! (スカラーと辞書の乗算)
\[\mathop{\mathrm{TotalDict}}\left[N;\mathbb{R}\right]\]
problem.infer(y / W)  # OK! (同一シェイプ (N, M) の配列同士の除算)
\[\mathop{\mathrm{Array}}\left[N\times M;\mathbb{R}\right]\]
problem.infer(S + s)  # OK! (同一キー集合を持つ全域辞書同士の加算)
\[\mathop{\mathrm{TotalDict}}\left[N;\mathbb{R}\right]\]

許容されない例#

try:
    # ERROR!(辞書と配列の乗算)
    problem.infer(S * y)
except Exception as e:
    print(e)
Traceback (most recent last):
    while inferring the type of expression `S * y`,
        defined at File "/tmp/ipykernel_750/3719066850.py", line 3, col 19-24
    while inferring the type of expression `S * y`,
        defined at File "/tmp/ipykernel_750/3719066850.py", line 3, col 19-24
    while checking if types `TotalDict[N; float]` and `Array[N, M; int!]` can be combined with numeric operator `*`,
        defined at File "/tmp/ipykernel_750/3719066850.py", line 3, col 19-24

File "/tmp/ipykernel_750/3719066850.py", line 3, col 19-24:

    3  |      problem.infer(S * y)
                            ^^^^^

error[E-TE0015] `numeric operator *` is not supported between types `TotalDict[N; float]` and `Array[N, M; int!]`

Hint: You can read the description and possible fix at https://jij-inc-jijmodeling.readthedocs-hosted.com/en/stable/error_codes/error/E-TE0015.html
try:
    # ERROR!(シェイプが異なる配列どうしの演算)
    problem.infer(y + z)
except Exception as e:
    print(e)
Traceback (most recent last):
    while inferring the type of expression `y + z`,
        defined at File "/tmp/ipykernel_750/3550267328.py", line 3, col 19-24
    while inferring the type of expression `y + z`,
        defined at File "/tmp/ipykernel_750/3550267328.py", line 3, col 19-24
    while checking if types `Array[N, M; int!]` and `Array[N, M, N; float!]` can be combined with numeric operator `+`,
        defined at File "/tmp/ipykernel_750/3550267328.py", line 3, col 19-24

File "/tmp/ipykernel_750/3550267328.py", line 3, col 19-24:

    3  |      problem.infer(y + z)
                            ^^^^^

error[E-TE0015] `numeric operator +` is not supported between types `Array[N, M; int!]` and `Array[N, M, N; float!]`

Hint: You can read the description and possible fix at https://jij-inc-jijmodeling.readthedocs-hosted.com/en/stable/error_codes/error/E-TE0015.html

代替記法:genarraygendict による配列の構築#

上の例では、y + z のように、非自明なブロードキャストを伴う演算は(意図的に)エラーになっていました。 このような場合、genarray()gendict() 関数を使い、陽にシェイプやキー集合と成分の式を指定することで、目的の配列・辞書を構築できるようになります:

A = jm.genarray(lambda i, j, k: y[i, j] + z[i, j, k], (N, M, N))
display(A)
problem.infer(A)
\[{\left( {y}_{i,j}+{z}_{i,j,k}\right) }_{\begin{subarray}{l} i\in \left\{0,\ldots ,N-1\right\}\\j\in \left\{0,\ldots ,M-1\right\}\\k\in \left\{0,\ldots ,N-1\right\}\end{subarray} }\]
\[\mathop{\mathrm{Array}}\left[N\times M\times N;\mathbb{R}\right]\]

また、Decorator API を利用している場合、以下のように内包表記を用いることもできます:

@problem.update
def _(problem: jm.DecoratedProblem):
    A = jm.genarray(y[i, j] + z[i, j, k] for i, j, k in (N, M, N))
    display(A)
    display(problem.infer(A))
\[{\left( {y}_{i,j}+{z}_{i,j,k}\right) }_{\begin{subarray}{l} i\in \left\{0,\ldots ,N-1\right\}\\j\in \left\{0,\ldots ,M-1\right\}\\k\in \left\{0,\ldots ,N-1\right\}\end{subarray} }\]
\[\mathop{\mathrm{Array}}\left[N\times M\times N;\mathbb{R}\right]\]

詳細は 配列・辞書に対する操作該当する説明部分を参照してください。

決定変数による除算について

モデルの構築の時点では、決定変数が現れうる式は加減乗除の左右どちらの辺にも現れることができます。 一方、これをインスタンスへとコンパイルする際には、決定変数が除法の右辺に現れる(上の例では N / x など)場合、現時点ではエラーになります。 これは、ソルバーによっては決定変数による除法を(特定のエンコードにより)サポートしている場合もあるので記法としては許容したい一方、現時点において JijModeling や OMMX がそうしたエンコード方法に対応していないためです。 将来的には、JijModeling や OMMX がこのようなエンコード方法の指定に対応し、一部のケースでは実際にインスタンスへとコンパイルできるようになる予定です。

初等超越関数

JijModeling の式では、加減乗除だけではなく、三角関数(sin(), cos(), tan()など)や対数関数(log2(), log10(), ln())などの初等超越関数もサポートしています。 これらの関数も決定変数の有無に関わらず式に適用できますが、現時点ではインスタンスへのコンパイル時に決定変数を含む式に適用されている場合はエラーになります。

比較式#

等値演算子(==, !=)や順序比較演算子(<, <=, >, >=)も、JijModeling の式に対して用いることができます。

これらの比較演算子の両辺が共に決定変数を含まない場合、結果は真偽値型 Bool の式として推論されます。一方、両辺の少なくとも一方が決定変数を含みうる場合、結果は特別な比較型として扱われます。これは、制約条件の定義では決定変数が現れる式同士を比較できる必要がある一方、内包表記などで使われる場合は真偽値が確定する比較式が使える必要があるためです。

数値型のスカラーには等値比較と順序比較の両方を利用できますが、カテゴリーラベルの値に利用できるのは ==!= のみです。 配列や辞書に比較演算子を適用する場合は、要素の型がその比較をサポートしていることに加え、算術演算と同様のシェイプやキー集合に関する条件を満たす必要があります。

problem.infer(x == y)  # OK! (スカラーと配列の等値比較)
\[\mathop{\mathrm{Comparison}}\left[\left\{0, 1\right\},\mathop{\mathrm{Array}}\left[N\times M;\mathbb{Z}\right]\right]\]
problem.infer(N <= N)  # OK! (スカラー同士の順序比較)
\[\mathrm{Bool}\]
problem.infer(y > W)  # OK! (同一シェイプ配列同士の比較)
\[\mathop{\mathrm{Comparison}}\left[\mathop{\mathrm{Array}}\left[N\times M;\mathbb{R}\right],\mathop{\mathrm{Array}}\left[N\times M;\mathbb{R}\right]\right]\]

論理演算#

JijModeling では、「論理積(かつ)」、「論理和(または)」「否定(でない)」などの論理演算を使って、複雑な条件式を表現することができます。 残念ながら、Python の andornot といった論理演算子はオーバーロードできないため、かわりにビット演算子 &(かつ)、|(または)、~(否定)や、関数jijmodeling.band()(かつ)、jijmodeling.bor()(または)、jijmodeling.bnot() を使って論理演算を表現します。

ビット演算の優先順位に注意!

and, or などと異なり、&|==!= よりも優先順位が高いため、たとえば a == b & c == d のように書くと a == (b & c) == d と解釈されてしまいます。 このため、&| を使う場合は、各比較式を (a >= b) & (c == d) のように常に括弧で囲むようにしてください。

以下は i が偶数または j が奇数の場合のみ和をとる例です:

@jm.Problem.define("Sum Example")
def problem(problem: jm.DecoratedProblem):
    N = problem.Length()
    M = problem.Length()
    a = problem.Float(shape=(N, M))
    x = problem.BinaryVar(shape=(N, M))
    problem += jm.sum(
        a[i, j] * x[i, j] for i in N for j in M if (i % 2 == 0) | (j % 2 == 1)
    )


problem
\[\begin{array}{rl} \text{Problem}\colon &\text{Sum Example}\\\displaystyle \min &\displaystyle \sum _{i=0}^{N-1}{\sum _{\substack{j=0\\i\bmod 2=0\lor j\bmod 2=1}}^{M-1}{{a}_{i,j}\cdot {x}_{i,j}}}\\&\\\text{where}&\\&\text{Decision Variables:}\\&\qquad \begin{alignedat}{2}{x}_{i,j}&\in \left\{0,1\right\}&\qquad &\text{a }2\text{-dim array of }\text{binary}\text{ decision variables}\\&\forall i\in \left\{0,\ldots ,N-1\right\},\;\forall j\in \left\{0,\ldots ,M-1\right\}&&\\\end{alignedat}\\&\\&\text{Placeholders:}\\&\qquad \begin{alignedat}{2}{a}_{i,j}&\in \mathbb{R}&\qquad &\text{a }2\text{-dim array of placeholders with elements in }\mathbb{R}\\&\forall i\in \left\{0,\ldots ,N-1\right\},\;\forall j\in \left\{0,\ldots ,M-1\right\}&&\\&&&\\M&\in \mathbb{N}&\qquad &\text{a scalar placeholder in }\mathbb{N}\\&&&\\N&\in \mathbb{N}&\qquad &\text{a scalar placeholder in }\mathbb{N}\\\end{alignedat}\end{array} \]

より複雑な条件式の例

論理演算を使ってより現実的かつ複雑な条件式を表現する例としては、JijZept 典型問題集の「ディープ・スペース・ネットワークのスケジューリング問題」が参考になるでしょう。