JijModeling の式と型#
JijModeling では、式を組み合わせてモデルを記述し、式はいくつかの種類=「型」に分類されます。 JijModeling はこの式の型の情報を Python の型ヒントに加え、独自のより詳細な検査を行う型システムを搭載しており、モデルの構築時に典型的な記述のミスを検出することが可能になっています。 個別の式の構築方法は次章以降で詳しく説明していきますので、本章ではその準備として JijModeling の「式」および「型」の概要について簡単に説明していきます。
Tip
以下では頻出と思われるパターンに絞って説明するため、式の構築に使える網羅的な一覧については、API リファレンスの Expression クラスや jijmodeling モジュールのトップレベル関数一覧を参照してください。
また、本サイトの Cheat Sheet には、さらに複雑な事例集がまとめられていますので、本章を読んだ後にそちらも参照するとよいでしょう。
import jijmodeling as jm
式とは#
JijModeling では、数理モデルの定義と入力データを分離することで、種々の機能や効率性を達成しています。 そのため、JijModeling を使ったモデリングでは、入力データを数理モデルに直接埋め込むのではなく、まず「入力データを与えられてはじめて具体的な数理モデルになるプログラム」を構築し、後から入力データを与えて数理モデルの具体例、すなわちインスタンスへとコンパイルするという流れになります。 この「入力データを与えられてはじめて具体的な数理モデルになるプログラム」を、JijModeling では式と呼んでいます。
より詳しく言えば、JijModeling の式は具体的な計算結果の値ではなく、決定変数やプレースホルダー、定数などを演算によってつなぎ合わせた「構文木」の形で保持されています。 次の例を考えましょう:
@jm.Problem.define("Test Problem")
def ast_examples(problem: jm.DecoratedProblem):
N = problem.Length()
x = problem.BinaryVar()
y = problem.IntegerVar(lower_bound=0, upper_bound=42, shape=(N,))
z = x + y[0]
w = jm.sum(y[i] for i in N)
display(repr(z))
display(repr(w))
'Expression(x + y[0])'
'Expression(sum(stream(N).map(lambda i: y[i])))'
図 5 Python 変数に束縛された決定変数、プレースホルダー、構文木#
図5は Test Problem の定義を可視化した図です。
\(x, y, N\) といった数理モデルに含まれる決定変数・プレースホルダーに対し、対応する Python 変数 x, y, N が定義されています。
このように、「変数」といったときにはそれがモデルに現れるパラメータなのか、それらを一時的に束縛している Python 変数なのかに曖昧性があるので、注意が必要です。
それらを使って定義された z = x + y[0] や w = jm.sum(y[i] for i in N) は、これらの変数を参照しながら作られた記号的な構文木として表現されているのです。
このように、JijModeling の「式」は、定数やプレースホルダー、決定変数などの個々の構成要素をさまざまな演算で組み合わせた形で表現されるのです。
式に対する関数呼び出しとメソッド呼び出しは同値
JijModeling では、Expression オブジェクト A に対する単項演算は、jm.log(A) のように前置式の関数呼び出しとして書くこともできますし、A.log() のように後置式のメソッド呼び出しで書くこともできます。
どちらも全く同じ式が構築されるようになっているため、好きな方を使って書くとよいでしょう。DecisionVar や Placeholder に対しても同様です。
ただし、Python の組込み数値などに対してはメソッド呼び出しができないため、こうした場合は関数呼び出しを用いて jm.log(2) のように書く必要があります。
JijModeling の「式の種類=型」#
JijModeling では、式は種類=型によって分類され、適宜検査されています。 JijModeling を使う上では、こうした型システムの詳細を理解せずとも使えるように設計されています。 一方で、数理モデルを定式化する上で、JijModeling がどのように型検査を提供しているのかを理解することは、依然として有用です。 そこで、本章では、JijModeling における式の型について簡単に触れておきます。
実は、JijModeling では以下の二段階で型検査を行っています:
Python の型ヒントによるエディタの補完・型検査支援
JijModeling 内蔵の型検査器による、モデル構築時の型検査
(1) はライブラリに Python コードとして同梱されており、 Pyright や ty、pyrefly といった代表的な型検査器によるエディタや Jupyter Notebook 上での補完・静的検査を可能にしています。
しかし、Python の型ヒントで表現できる制約には制限があり、たとえば配列の添え字サイズの検証などには不向きです。こうした表現力の不足を補うため、JijModeling は (2) の独自の型検査器も内蔵しています。
(2) の型検査器は Python のユーザーが直接呼び出すものではなく、モデルへの制約条件や目的関数項の追加、決定変数・プレースホルダーの shape の宣言などの際に適宜呼び出され、記述の誤りがないかをデータの入力前に自動的に検証します。
Python の型ヒントでも Expression、Placeholder、DecisionVar などの API オブジェクトは区別されます。
しかし、式のシェイプや辞書のキー集合、決定変数を含むかどうかといった詳細な情報までは表現しきれないため、JijModeling が搭載する型検査器は、こうした情報も含めて検査しています。
つまり、エディタや Jupyter Notebook 上では Python の型ヒントに基づく比較的「粗い」基準で検査し、モデルの構築中には JijModeling がより細分化された基準で検査するという二段構えです。
JijModeling が搭載している式の型はいくつかありますが、代表的なものを以下にまとめます:
種類 |
数式(表記例) |
テキスト表記例 |
説明 |
|---|---|---|---|
数値型 |
\(\mathbb{N}, \mathbb{Z}, \mathbb{R}\) |
|
自然数・整数・実数などの数値を表す型。 |
カテゴリーラベル型 |
\(L\) |
|
ユーザーが後から追加するラベルの集合。 |
多次元配列型 |
\(\mathrm{Array}[N_1, \ldots, N_k; A]\) |
|
|
辞書型 |
\(\mathrm{TotalDict}[K; V]\) / \(\mathrm{PartialDict}[K; V]\) |
|
キー集合 \(K\) と値型 \(V\) を持つ辞書の型。 |
タプル型 |
\(T \times U\) |
|
成分ごとに型を持つ固定長タプルの型。 |
これらを念頭に、以下では数理モデルの定式化でよく現れる演算について順に見ていきましょう。
エラーになるタイミング
JijModeling 内蔵の型検査は、式が構築された直後ではなく、以下のタイミングで行われます:
数理モデルの目的関数に項が追加されたとき
Problem.Constraint()により制約条件が宣言されたときndim,shapeやdict_keysの成分として現れたときProblem.eval()関数やCompilerによりインスタンスへコンパイルされるときProblem.infer()関数により明示的に型推論を行わせたとき
これは、式が文脈に置かれて初めて適切な「型」が定まるためです。 そのため、以下で見ていく式の構築方法について「不正」な記述であっても、単に式を構築した段階でエラーになるとは限らないことに注意してください。
以下では、妥当な用例や妥当でない用例を例示するために、Problem.infer() メソッドを用いています。
このメソッドは、Problemの持っている決定変数・プレースホルダーの情報を基に、与えられた式の型を推論するメソッドであり、不正な式を与えると型エラーを発生させます。
例を見てみましょう。ここでは、バイナリ変数 \(x\) と整数 \(N\) を足しているので、\(x + N\) は整数型 \(\mathbb{Z}\)を持つものとして推論されています。
problem = jm.Problem("Type Inference Example")
x = problem.BinaryVar("x", description="スカラーの決定変数")
N = problem.Integer("N")
problem.infer(x + N) # OK! スカラー同士の足し算
一方で、スカラー値と文字列は足し算できないため、次の例はエラーとなります。
try:
# エラー!文字列とスカラーは足し算できない
problem.infer(x + "hoge")
except Exception as e:
print(e)
Traceback (most recent last):
while inferring the type of expression `x + "hoge"`,
defined at File "/tmp/ipykernel_816/2391251849.py", line 3, col 19-29
while inferring the type of expression `x + "hoge"`,
defined at File "/tmp/ipykernel_816/2391251849.py", line 3, col 19-29
while checking if types `binary!` and `Literal["hoge"]` can be combined with numeric operator `+`,
defined at File "/tmp/ipykernel_816/2391251849.py", line 3, col 19-29
File "/tmp/ipykernel_816/2391251849.py", line 3, col 19-29:
3 | problem.infer(x + "hoge")
^^^^^^^^^^
error[E-TE0015] `numeric operator +` is not supported between types `binary!` and `Literal["hoge"]`
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
Expression と ExpressionLike / ExpressionFunction の関係は?
API リファレンス やエディタの補完・ドキュメント上では、ExpressionLike や ExpressionFunction といった型名が登場します。
これらはライブラリの実装には存在しないダミーの略記用の型であり、Expression に変換可能な型や、Expression から Expression への関数を表す型の略記です。
具体的には以下のように考えておけば大丈夫です:
型名 |
説明 |
|---|---|
|
|
一つ以上の |
式としてのプレースホルダー、決定変数#
「JijModeling における変数」章で見たように、JijModeling では Problem.BinaryVar や Problem.Placeholder などによって決定変数やプレースホルダーを定義します。
この際に返されるのは、それぞれの変数のメタデータを保持する DecisionVar や Placeholder オブジェクトですが、これらは式の構築中に現れると、自動的に Expression オブジェクトへと変換されます。
上の Test Problem の例でも、Python 変数 x や y はそれぞれ DecisionVar オブジェクトですが、それを用いて z = x + y[0] などのように構築されると、x, y はそれぞれ決定変数と決定変数の配列を表す式に変換されています。
また、z の定義中に現れる 0 は通常の Python の数値ですが、このような定数も JijModeling の式中に現れると自動で変換されるようになっています。
式のつくりかた#
これ以降では、次の数章に分けて具体的な式の構築方法を見ていきます。
- 算術式と条件式
加減乗除などの算術演算や、大小・同値性比較などによる式の構築方法を紹介します。
- 配列・辞書に対する操作
多次元配列や辞書の宣言や要素へのアクセス方法などを説明します。
- 畳み込みとストリーム
ストリームを用いて配列や辞書を畳み込む方法や、論理演算を用いた式の構築方法を紹介します。
また、これらの構文の具体的な用例については、Cheat Sheet が参考になるでしょう。