配列・辞書に対する操作#

ユーザーが入力する変数:プレースホルダーとカテゴリーラベルソルバーが決定する変数:決定変数では、各種変数の配列や辞書を定義する方法を扱いました。 JijModeling では、こうした変数から成るものに限らず、一般の要素を持つ配列や辞書(以下まとめて「コレクション」と呼びます)を扱うことができます。 こうした配列・辞書などの詳しい概念的な説明や使い分けの基準については JijModeling における変数 章の「変数の配列と辞書」節で説明していますので、そちらを参照してください。

以下では、各種コレクションの概念について改めて復習した後、それらを生成する関数や要素へのアクセス方法を見ていきます。 また、次章「畳み込みとストリーム」では、更に配列や辞書をストリームとして総和や総積を取る方法についても触れます。

import jijmodeling as jm

コレクションの生成:genarray()gendict()#

配列や辞書は、ユーザーが入力する変数:プレースホルダーとカテゴリーラベルソルバーが決定する変数:決定変数で説明したように、変数宣言時に導入することもできますが、他の式を使って新たに生成することもできます。 配列の生成に使うのが genarray()関数、辞書の生成に使うのが gendict()関数です。

配列の生成関数:genarray()#

genarray() は NumPy の fromfunction() に類似する関数[1]であり、シェイプと添え字から要素への関数(生成関数)を与えることで、新しい配列を生成することができます。 以下では、genarray を用いて、シェイプ \((N, M)\) の各添え字毎の和を要素に持つ配列を生成しています:

problem = jm.Problem("Array and Dict Example")
N = problem.Length("N")
M = problem.Length("M")

jm.genarray(lambda i, j: i + j, (N, M))
\[{\left( i+j\right) }_{\begin{subarray}{l} i\in \left\{0,\ldots ,N-1\right\}\\j\in \left\{0,\ldots ,M-1\right\}\end{subarray} }\]

また、Decorator API 内では内包表記を使って簡潔に書くこともできます:

@problem.update
def _(problem: jm.DecoratedProblem):
    display(jm.genarray(i + j for (i, j) in (N, M)))
\[{\left( i+j\right) }_{\begin{subarray}{l} i\in \left\{0,\ldots ,N-1\right\}\\j\in \left\{0,\ldots ,M-1\right\}\end{subarray} }\]

ここで in の右辺に現れるタプル (N, M) は、NM の直積集合を表す省略記法です。 この記法については「畳み込みとストリーム」章で改めて説明します。

genarray() で利用できる内包表記は、ただ一つの for 節のみをサポートしており、また if 節は使えません。 たとえば、以下のように複数の for 節を使ってしまうと、エラーとなります:

try:

    @jm.Problem.define("genarray example")
    def problem(problem):
        N = problem.Natural()
        M = problem.Natural()
        a = problem.Float(shape=(N, M))
        x = problem.BinaryVar(shape=N)
        Sums = problem.NamedExpr(jm.genarray(a[i, j] * x[i] for i in N for j in M))

except SyntaxError as e:
    print(str(e))
error[E-SE0002] A `genarray` comprehension must have exactly one for-clause.

Possible fix: use a single `for` that iterates over the whole shape or key set at once.

File "/tmp/ipykernel_771/1800573765.py", line 9, col 46-82:

    9  |          Sums = problem.NamedExpr(jm.genarray(a[i, j] * x[i] for i in N for j in M))
                                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

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

辞書の生成関数:gendict()#

辞書の生成関数 gendict() も、キーの集合を表す式と、キーから値への生成関数を与えることで、新しい辞書を生成することができます。 この生成関数は必ず値を返すため、gendict() により生成される辞書は常に TotalDict となります。

以下では、カテゴリーラベル\(L\)と自然数\(N\)をキーとする辞書を生成しています:

problem = jm.Problem("Array and Dict Example")
N = problem.Natural("N")
L = problem.CategoryLabel("L")
x = problem.BinaryVar("x", dict_keys=L)
jm.gendict(lambda l, n: x[l] + n, (L, N))
\[{\left\{ {x}_{l}+n\right\} }_{\begin{subarray}{l} l\in L\\n\in N\end{subarray} }\]

gendict() も Decorator API ではただ一つの for 節と任意個の if 節からなる内包表記をサポートしています。

@problem.update
def _(problem: jm.DecoratedProblem):
    display(jm.gendict(x[l] + n for (l, n) in (L, N) if n % 2 == 0))
\[{\left\{ {x}_{l}+n\right\} }_{\begin{subarray}{l} l\in L\\n\in \left\{0,\ldots ,N-1\right\}\\n\bmod 2=0\end{subarray} }\]

配列・辞書の定義域の取得#

PlaceholderDecisionVar オブジェクトでは、配列のシェイプを表すタプルを shape 属性から取得できます。一方、一般の式については Expression.shape() メソッドを使います。 辞書のキー集合を表す式は Expression.keys() メソッドで取得できます。また、配列式に対しては、シェイプの \(n\) 番目を取得するための Expression.len_at(n) メソッドも用意されています。 これらは、数理モデルの定式化の際に、定義域を走査する総和や制約条件を定義する際などに使うことができます。

添え字による要素アクセスとスライス#

Python の組み込みのリストや辞書、あるいは numpy.ndarray と同様、JijModeling の式でも x[i, j] のように多次元の添え字(インデックス)を用いてコレクションの要素にアクセスすることができます。 具体的には、JijModeling では次の型を持つ式に対して添え字を用いることができます:

  1. (多次元)配列

    • 許容される添え字:決定変数を含まない自然数型の式

  2. 辞書

    • 許容される添え字:辞書のキー型に一致する、決定変数を含まない式。整数、文字列、カテゴリーラベル、またはそれらから成るタプルを指定できます。

  3. タプル

    • 許容される添え字:決定変数を含まず成分数内の自然数型の式

いずれの場合も、添え字に決定変数を含めることはできません。 以下は、配列と辞書に対して添え字を用いて要素にアクセスする例です:

import jijmodeling as jm


@jm.Problem.define("Array and Dict Example")
def problem(problem: jm.DecoratedProblem):
    N = problem.Natural()
    L = problem.CategoryLabel()

    w = problem.Float(shape=N)  # N要素配列
    x = problem.BinaryVar(dict_keys=(N, L))  # 辞書

    problem += jm.sum(w[i] * x[i, l] for i in N for l in L)


problem
\[\begin{array}{rl} \text{Problem}\colon &\text{Array and Dict Example}\\\displaystyle \min &\displaystyle \sum _{i=0}^{N-1}{\sum _{l\in L}{{w}_{i}\cdot {x}_{i,l}}}\\&\\\text{where}&\\&\text{Decision Variables:}\\&\qquad \begin{alignedat}{2}{x}_{i,j}&\in \left\{0,1\right\}&\qquad &\text{a dictionary of }\text{binary}\text{ decision variables}\\&\forall i\in \left\{0,\ldots ,N-1\right\},\;\forall j\in \mathrm{L}&&\\\end{alignedat}\\&\\&\text{Placeholders:}\\&\qquad \begin{alignedat}{2}N&\in \mathbb{N}&\qquad &\text{a scalar placeholder in }\mathbb{N}\\&&&\\{w}_{i}&\in \mathbb{R}&\qquad &\text{a }1\text{-dim array of placeholders with elements in }\mathbb{R}\\&\forall i\in \left\{0,\ldots ,N-1\right\}&&\\\end{alignedat}\\&\\&\text{Category Labels:}\\&\qquad \begin{array}{rl} L&\text{Category Label}\end{array} \end{array} \]

添え字は x[i,j,k] のように複数成分を同時に書くことができますが、タプルの成分数や、配列の次元、辞書のタプル長を越える添え字を用いると以下のように型エラーとなります。

import jijmodeling as jm


@jm.Problem.define("Array and Dict Example, oversubscripted")
def problem(problem: jm.DecoratedProblem):
    N = problem.Natural()
    M = problem.Natural()

    w = problem.Float(shape=(N, M))  # N × M 配列

    try:
        problem += jm.sum(w[i, j, i] for i in N for j in M)  # ERROR! 添え字が多すぎる
    except Exception as e:
        print(e)
Traceback (most recent last):
    while checking if expression `sum(stream(N.flat_map(lambda i: M.map(lambda j: (i, j)))).map(lambda (i, j): w[i, j, i]))` has type `float!`,
        defined at File "/tmp/ipykernel_771/3976890897.py", line 12, col 20-60
    while inferring the type of expression `sum(stream(N.flat_map(lambda i: M.map(lambda j: (i, j)))).map(lambda (i, j): w[i, j, i]))`,
        defined at File "/tmp/ipykernel_771/3976890897.py", line 12, col 20-60
    while inferring the type of expression `sum(stream(N.flat_map(lambda i: M.map(lambda j: (i, j)))).map(lambda (i, j): w[i, j, i]))`,
        defined at File "/tmp/ipykernel_771/3976890897.py", line 12, col 20-60
    while inferring the type of expression `stream(N.flat_map(lambda i: M.map(lambda j: (i, j)))).map(lambda (i, j): w[i, j, i])`,
        defined at File "/tmp/ipykernel_771/3976890897.py", line 12, col 27-59
    while inferring the type of expression `stream(N.flat_map(lambda i: M.map(lambda j: (i, j)))).map(lambda (i, j): w[i, j, i])`,
        defined at File "/tmp/ipykernel_771/3976890897.py", line 12, col 27-59
    while checking if the type of expression `lambda (i, j): w[i, j, i]` is a function with domain `Tuple[natural, natural]`,
        defined at File "/tmp/ipykernel_771/3976890897.py", line 12, col 27-59
    while inferring the type of expression `lambda (i, j): w[i, j, i]` under application with argument types `Tuple[natural, natural]`,
        defined at File "/tmp/ipykernel_771/3976890897.py", line 12, col 27-59
    while inferring the type of expression `w[i_2157326463, j_63006429, i_2157326463]`,
        defined at File "/tmp/ipykernel_771/3976890897.py", line 12, col 27-37
    while checking if type `Array[N, M; float]` can be subscripted with (i_2157326463, j_63006429, i_2157326463): (natural, natural, natural),
        defined at File "/tmp/ipykernel_771/3976890897.py", line 12, col 27-37

File "/tmp/ipykernel_771/3976890897.py", line 12, col 27-37:

    12  |          problem += jm.sum(w[i, j, i] for i in N for j in M)  # ERROR! 添え字が多すぎる
                                     ^^^^^^^^^^

error[E-TE0018] Too many subscripts: the array has 2 dimension(s) (shape `[N, M]`), but 3 subscript(s) were given (types: `natural, natural, natural`).

Possible fix: give the array at most one subscript per dimension.

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

配列の添え字では、更にx[:, 1] のようなスライス記法を用いることができます。

import jijmodeling as jm


@jm.Problem.define("Slicing example")
def problem(problem: jm.DecoratedProblem):
    N = problem.Natural()
    M = problem.Natural()

    w = problem.Integer(shape=N)  # N 要素配列
    x = problem.BinaryVar(shape=(N, M))  # N × M 配列

    problem += problem.Constraint("sum-per-n", [x[i, :].sum() == w[i] for i in N])


problem
\[\begin{array}{rl} \text{Problem}\colon &\text{Slicing example}\\\displaystyle \min &\displaystyle 0\\&\\\text{s.t.}&\\&\begin{aligned} \text{sum-per-n}&\quad \displaystyle \sum _{{i}_{1}=0}^{M-1}{{x}_{i,\left(\colon \right),{i}_{1}}}={w}_{i}\quad \forall i\;\text{s.t.}\;i\in \left\{0,\ldots ,N-1\right\}\end{aligned} \\&\\\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}M&\in \mathbb{N}&\qquad &\text{a scalar placeholder in }\mathbb{N}\\&&&\\N&\in \mathbb{N}&\qquad &\text{a scalar placeholder in }\mathbb{N}\\&&&\\{w}_{i}&\in \mathbb{Z}&\qquad &\text{a }1\text{-dim array of placeholders with elements in }\mathbb{Z}\\&\forall i\in \left\{0,\ldots ,N-1\right\}&&\\\end{alignedat}\end{array} \]

また、x[1, 1:N:2]のようにステップ数や終了インデックスを指定するスライスもサポートしています。 スライス記法の詳細については、Python 公式ドキュメントの「スライス表記 (slicing)」を参照してください。