【予告篇】JijModeling 2: 新たなる記法
この記事は Jij Advent Calendar 2025 の9日目の記事です。



はじめに:JijModelingとは?
……はい、すみません。というわけで、本記事ではJijModeling 2の新機能についてご紹介していきたいと思います。
JijModelingはJijが開発する無償の数理最適化モデリング領域特化言語であり、Pythonライブラリとして提供されています。Jijの統合数理最適化プラットフォームであるJijZeptで数理モデルの記述用ライブラリとして提供されています。
また、PyPI にアップロードされているため、Python パッケージとして無償でご利用頂けます。
JijModeling は次のような特徴を持ちます:
- 直感的な数式の記述
- 既存のPythonエコシステムとのシームレスな統合
- データとモデルの分離
- 制約検出による求解の自動高速化
- ソルバ非依存
たとえば、以下は JijModeling 1 によるナップザック問題の記述例です:
import jijmodeling as jm
# 問題の定義
problem = jm.Problem("Knapsack Problem", sense=jm.ProblemSense.MAXIMIZE)
## 計算前に確定しているパラメータ(「プレースホルダ」)の定義
N = jm.Placeholder("N", dtype=jm.DataType.INTEGER, description="商品の個数")
w = jm.Placeholder("w", shape=(N,), description="各商品の重さ")
v = jm.Placeholder("v", shape=(N,), description="各商品の価値")
W = jm.Placeholder("W", description="ナップザックの耐荷重")
i = jm.Element("i", belong_to=N)
## 決定変数の定義
x = jm.BinaryVar("x", shape=(N,))
## 制約条件
## 添え字に当たるパラメータを定義しておく
problem += jm.Constraint(
"weight limit",
jm.sum([i], w[i] * x[i]) <= W,
)
## 目的関数:価値の最大化
problem += jm.sum([i], x[i] * v[i])
jm.Element()などやや不自然な記法はありますが、ほぼ数式を「そのまま」書けていることがわかります。
また、JijModelingはPythonエコシステムとシームレスに統合されており、Jupyter Notebookで上のプログラムを評価し、最後に problem を表示させてみると、以下のように数式でモデルを確認できます。

大事なことは、この時点ではモデルはあくまでも数式として記述されており、実データと切り離して定式化されていることです。
多くのモデラーでは、データとモデルがいっしょくたに定義されるため上記のような数式プレビューは難しく、また定義を一部変更するだけでもループが回ってしまい不必要に時間がかかってしまいます。
また、JijModelingはオープンソースの数理最適化問題フォーマットOMMXへのコンパイラとして実装されているため、特定のソルバに依存せずに依存せずに数理最適化モデルを完結に定義できます。
JijModelingはこのような記号表現を基に、特殊な形状の制約条件を検出しソルバーに情報を伝えるため、自動的に大幅に求解時間の高速化が達成できます。
詳細については、昨年の人工知能学会の講演資料を御覧ください。
なぜ JijModeling 2?
上記のようにユニーク特色を備えたJijModelingですが、実は現在大幅なアップデートを施した次期バージョンJijModeling 2の開発が進められており、執筆時ではPyPI上では beta.6 までリリースされています。
JijModeling 2 は JijModeling 1 の精神を受け継ぎつつ、構文面も含めて完全にゼロから作り直されたものです。
なぜ親しまれてきた JijModeling をゼロから創り直す必要があったのか?それは、JijModelingを本物のプログラミング言語の域にまで持ち上げるためです。
具体的には、JijModeling 2は以下を念頭に置いてゼロから再設計されています:
- 数式としてより「自然」な記法、より「Pythonっぽい」構文のサポート
- プレースホルダや決定変数をProblemに保存するように
- 多重配列などのより自然な振る舞いと追加演算のサポート
- 専用の型検査器による実行前のモデルのバリデーション
- 辞書型やラベル型、タプル型などによるより柔軟な定式化のサポート
以下、各項目について、例をとりながら簡単に議論していければと思います。
進化1:より自然な「数式」、より「Pythonっぽい」構文
JijModeling 1系の記法の中で一番不自然な記法としてまず槍玉に挙げられるのが、総和や添え字で現れるElementという型の存在です。
JijModelingでは、上の Knapsack の例で見たように、総和の範囲などを指定するには i = Element("i", belong_to=N) のように、「ある集合(ここではElementを宣言する必要がありました。
また、単なる総和ではなく、
jm.sum([(i, i % 2 == 0)], w[i] * x[i])
できればこれは内包表記を使って jm.sum(w[i] * x[i] for i in N if i % 2 == 0) のように書けると嬉しい、というのが人情だと思います。
結論から言えば、なんと、JijModeling 2 では次のようにしてより「自然」な形でナップザック問題を記述できます!
import jijmodeling as jm
# 問題の定義
@jm.Problem.define("Knapsack Problem", sense=jm.ProblemSense.MAXIMIZE)
def problem(problem: jm.DecoratedProblem):
## Placeholder / BinaryVar などは、problemに対して登録する形で作成するように
## 名前が省略されている!
N = problem.Length(description="商品の個数")
w = problem.Float(shape=(N,), description="各商品の重さ")
v = problem.Float(shape=(N,), description="各商品の価値")
# もちろん、明示的に名前を指定することも可能
W = problem.Float("W", description="ナップザックの耐荷重")
## 決定変数の定義
x = problem.BinaryVar(shape=(N,))
## 制約条件
problem += problem.Constraint(
"weight limit",
# 内包表記!
# もう Element の定義は不要!
jm.sum(w[i] * x[i] for i in N) <= W,
# 制約条件の説明文も書けるようになりました
description="荷重上限を越えない"
)
## 目的関数:価値の最大化
## 内包表記を使わずに、単純に同じ形状の配列同士を要素ごとに積をとって、
## その和を取る、という形でも書ける
problem += jm.sum(x * v)
JijModeling 2 ではこのように @jm.Problem.define のブロックを使うと、その中では内包表記を使って和などを定義できるようになります。
また、内包表記内では if 文が使えるため、先程の和は JijModeling 2 では以下のようにより簡潔に書くことができます:
jm.sum(w[i] * x[i] for i in N if i % 2 == 0)
更に、よくみると一部のパラメータの名前も省略されているのに気付くでしょう。
たとえば、旧来はx = jm.BinaryVar("x", shape=(N,)) のように、xという決定変数に対して、左辺の「Pythonの変数」としてxと、「決定変数の名前」としての "x" が重複して宣言されていましたが、JijModeling 2 ではx = problem.BinaryVar(shape=(N,)) のようにdefineブロックの内側では名前が省略できるようになっています。
もちろん、W = problem.Float("W", description="ナップザックの耐荷重")のように、これまで通り陽に変数名を指定することもできます。
これをJupyter Notebookで評価すると次のような出力になります:

先程と同値な定義が得られ、またPlaceholderなど JijModeling 1 よりも詳細な情報が出力されているのがわかるでしょう。
進化2:プレースホルダや決定変数をProblemに保存するように
さて、JijModeling 1と2でのナップザック問題の定式化をよく見比べてみると、プレースホルダや決定変数をの作成方法が変わっている事に気付くでしょう。
たとえば、JijModeling 1では以下のようにしてこれらのパラメータを定義していました:
## 計算前に確定しているパラメータ(「プレースホルダ」)の定義
N = jm.Placeholder("N", dtype=jm.DataType.INTEGER, description="商品の個数")
w = jm.Placeholder("w", shape=(N,), description="各商品の重さ")
v = jm.Placeholder("v", shape=(N,), description="各商品の価値")
W = jm.Placeholder("W", description="ナップザックの耐荷重")
import jijmodeling as jm としていますので、jmは JijModeling のトップレベルモジュールです。
これに対して、JijModeling 2では以下のようにProblemに対してPlaceholder等を定義しています:
@jm.Problem.define("Knapsack Problem", sense=jm.ProblemSense.MAXIMIZE)
def problem(problem: jm.DecoratedProblem):
## Placeholder / BinaryVar などは、problemに対して登録する形で作成するように
## 名前が省略されている!
N = problem.Length(description="商品の個数")
w = problem.Float(shape=(N,), description="各商品の重さ")
v = problem.Float(shape=(N,), description="各商品の価値")
# もちろん、明示的に名前を指定することも可能
W = problem.Float("W", description="ナップザックの耐荷重")
これは略記法ではなく、従来のように直接 jijmodeling モジュールからPlaceholderやDecisionVarを作成しようとするとJijModeling 2ではエラーになります。
このような設計になっているのは、主に二点に依ります:
- 同じ最適化問題内での変数名の衝突による予期しない挙動を防ぐため
- 最適化問題の「変換」を手軽に行えるようにするため
旧来のJijModeling 1では、プレースホルダや決定変数の情報は、構文木中のノードとして直接保存していました。
このため、原理的には同じ名前で違う名前の変数を複数宣言してしまい、両者を混ぜて使ってしまう可能性がありました。
もちろん、JijModeling 1でもProblemの更新時に名前の衝突がないかのチェックはしていましたが、新機能追加の際にチェックが漏れうる可能性も常にありました。
そこで、JijModeling 2ではまずProblemに紐付けて変数を宣言するようにすることで、変数宣言の段階で衝突は一律エラーとすることで斉一的に対処できるようになりました。
また、離散変数の連続緩和など、最適化問題の変換を行う場合、旧来のJijModeling 1系統では構文木を隅々まで舐めて、指定された離散変数の出現があるたびにそれを連続変数に置き換える必要がありました。
これに対してJijModeling 2では、変換前のProblemとは別のProblemを作成し、決定変数の宣言を差し替えるだけで、あとは残りの式を再利用することでこうした処理が手軽に実装できるようになります。
進化3:多重配列などのより自然な振る舞いと追加演算のサポート
従来の JijModeling では、決定変数やプレースホルダに対して、多次元配列を用いることができました。
JijModeling 1系統では、基本的に式は最終的に数値に変換されるようなものだけが叶えられており、配列の演算も部分的にしかサポートされていませんでした。
サポートされている例としては、以下のように添え字と組み合わせることで、決定変数の配列に対する総和を取ってやることができました:
x = jm.BinaryVar(shape=(N,))
problem += x[:].sum()
しかし、後置している添え字 [:] を省略するとこのプログラムは通らず、また Placeholder に対してはこのような記法は使えなかったりするなど、あくまでも限定的な機能しか提供されていませんでした。
JijModeling 2ではこうした扱いを改善し、配列も第一級の値として扱えるようになります。
たとえば、上の例のように x[:].sum()により決定変数の配列 x の総和を取ることもできますし、なんなら [:] を省略して x.sum() や jm.sum(x)と書くこともできるようになりました。更に、プレースホルダの配列や、それらから計算される一般の配列式に対しても区別なく .sum()を使うことができるようになっています。
例として、前掲の JijModeling 2 によるナップザック問題の耐荷重の制約条件の記述を思い出してみましょう:
problem += problem.Constraint(
"weight limit",
jm.sum(w[i] * x[i] for i in N) <= W,
description="荷重上限を越えない"
)
この例は内包表記を使っていますが、これくらい単純な例であれば、配列上の掛け算と総和を使って簡潔に書くこともできます:
problem += problem.Constraint(
"weight limit",
jm.sum(w * x) <= W, # (w * x).sum() <= W とも書ける
# 制約条件の説明文も書けるようになりました
description="荷重上限を越えない"
)
より詳しく、配列同士の四則演算は以下のような場合がサポートされています:
- スカラーと配列同士の演算
- 同じシェイプの配列同士の演算
ここでは、w はシェイプ N のプレースホルダ、x はシェイプ Nの決定変数であり、両者のシェイプは一致しています。
こうした同一形状の演算は、numpyにおける同種の演算(ブロードキャストと呼ばれます)と同様に、同じ位置にある要素同士の演算として計算されます[1]。
なので、ここでは同じ位置の成分同士を掛け合わせた配列へと評価され、sumにより総和が取られて最初の例と同じ結果が得られるわけです。
他にも、mapやfilterなどnumpyでも提供されている高階関数も提供しており、上の例は、更に以下のように書いても同じになります:
problem += problem.Constraint(
"weight limit",
N.map(lambda i: w[i] * x[i]).sum() <= W,
description="荷重上限を越えない"
)
このように、JijModeling 2では配列上の演算が拡充され、柔軟な記述が可能になっています。
進化4:専用の型検査器による実行前のモデルのバリデーション
JijModeling 最大の特徴は、モデルとデータの分離でした。これによる恩恵は幾つかありましたが、特に実データよりも前に式の正当性を検証でき、数万パラメータを流し込む前にあるていどモデルのサニティチェックができる、というのが大きな強みでした。
また、JijModelingはPythonの型ヒントを提供しており、コードを書く際にIDEによって型に基づいたバリデーションが行えるようになっていました。
JijModeling 2ではこれを更に推し進め、JijModeling独自の型システムを搭載するようになりました!
Pythonの型ヒントもこれまで以上により詳細なものが与えられていますが、Pythonレベルの型検査だけではなく、数理モデルの構築の過程で JijModeling が内部で常に型検査を行い、論理的な誤りをより早期に検出できるようになりました。
このため、JijModeling 2 では、Placeholderに対しても型を宣言することが必須となります。
この変更一見面倒に思えるかもしれませんが、さまざまな用途に特化したスマートコンストラクタが提供されていますので、パラメータの「意図」をより明確にする意味でもPlacholderの型宣言をする方がよりモデルの意図が明らかになるという利点があります。
旧来の JijModeling では決定変数の「型」は(BinaryVarやContinuousVarなどの形で)宣言していましたので、その延長上で理解できるものと思っています。
実際、上の JijModeling 2 によるナップザック問題の定式化では、この型が特化されたプレースホルダの構築子を使っています:
## Placeholder / BinaryVar などは、problemに対して登録する形で作成するように
## 名前が省略されている!
N = problem.Length(description="商品の個数")
w = problem.Float(shape=(N,), description="各商品の重さ")
v = problem.Float(shape=(N,), description="各商品の価値")
# もちろん、明示的に名前を指定することも可能
W = problem.Float("W", description="ナップザックの耐荷重")
これは、実際には以下のように書くのと同値になっています:
## Placeholder / BinaryVar などは、problemに対して登録する形で作成するように
## 名前が省略されている!
N = problem.Placeholder(description="商品の個数", dtype=jm.DataType.NATURAL)
w = problem.Placeholder(shape=(N,), description="各商品の重さ", dtype=float)
v = problem.Placeholder(shape=(N,), description="各商品の価値", dtype=float)
# もちろん、明示的に名前を指定することも可能
W = problem.Placeholder("W", description="ナップザックの耐荷重", dtype=float)
旧来は Placeholder を dtypeなしで宣言すると浮動小数点数値のパラメータとして解釈されていましたが、JijModeling 2以降はdtypeの指定が必須になっています。
dtypeの指定を読んだり書いたりするのは手間がかかるので、この観点からも problem.Length や problem.Integer などの、特化されたプレースホルダを利用することを推奨しています。
このように型を明確化することで、コードの意図が明確になって人間の可動性があがるだけでなく、AIによるコード生成の精度の向上や、人間がモデルを記述する際にデータ結合前の時点での早期のフィードバックが可能になる予定です。
進化5:辞書型やラベル型、タプル型などによるより柔軟な定式化のサポート
JijModeling 1 系統ではパラメータの多重配列や、シェイプ定まらないJagged Arrayなどによって様々なモデルが定式化されていました。
Jagged Array はトリッキーなモデルの記述に使えた一方で、範囲外アクセスが実行時までわからなかったり、モデルの本来の構造と表現の関係が一見して理解しづらく、意図が不明瞭になるなどの問題点もありました。
こうした観点から、JijModeling 2 では Jaggad Array の機能自体は提供しつつも非推奨とし、かわりに配列以外の様々なデータ構造を提供することでより可読性と表現力を向上することを企図しています。
たとえば、Jagged Arrayは0-originでなかったり値が連続していないような数列で添え字づけられた構造を表すのに使われていましたが、JijModeling 2 では辞書とラベル型を組み合わせることで自然に表現できます。
辞書型は、Pythonの辞書型のようなもので、文字列または整数、あるいはそれらからなるタプルを鍵とし、それに関連づけられた値を保存するデータ構造です。ラベル型は、いわゆるカテゴリカル変数に相当するもので、具体的な定義はコンパイル時まで確定せず、モデル記述時は同値性や非同値性の比較のみができるようなものです。
わかりづらいと思いますので、ここではシナジーボーナスつきの配送計画問題を考えてみましょう。これは、基本的には通常の配送計画問題と同様に利用できるトラックの一覧と荷物の一覧が与えられたときに、トラック容量の制約を満たしつつ価値を最大化するような問題ですが、一部の荷物の組を同一のトラックで運ぶとボーナスが生じるような問題です。
これのJijModeling 2での定式化は以下のようになります:
@jm.Problem.define("QuadraticKnapsackLogistics", sense=jm.ProblemSense.MAXIMIZE)
def _(problem: jm.DecoratedProblem):
# 荷物とトラックのラベルを表す不透明な「カテゴリラベル」を定義
# これらは整数または文字列の集合として扱われます
I = problem.CategoryLabel("I", description="荷物のラベル")
J = problem.CategoryLabel(description="トラックのラベル")
# デフォルトではコンパイラは、そのドメインのすべてのキーに対して値が定義されていることを期待します
weights = problem.Integer(
"w", dict_keys=I, description="各荷物の重さ"
)
base_revenues = problem.Integer(
"r", dict_keys=I, description="各荷物の基本利益"
)
capacities = problem.Integer(
"C", dict_keys=J, description="各トラックの荷重容量"
)
# `partial_dict=True`を設定すると、辞書をキーのサブセット上でのみ定義できるようになります
# ここで`s`は実際にシナジーボーナスがある荷物のペアに対してのみ定義されます
synergy_bonuses = problem.PartialDict(
"s",
keys=(I, I),
description="荷物のペア間のシナジーボーナス",
)
# --- 4. 決定変数 ---
# 決定変数の数はプレースホルダーから静的に決定される必要があるため、
# 決定変数の辞書は全キードメイン(全域)で定義される必要がある
x = problem.BinaryVar(
dict_keys=(I, J),
description="荷物iをトラックjに割り当てる場合x[i,j] = 1、そうでない場合0",
)
# --- 5. 目的関数 ---
problem += jm.sum(
synergy_bonuses[i, k] * x[i, j] * x[k, j]
for j in J
# keys()でキーを、
# items()でキー値ペアを、
# values()で値をイテレートできます
for (i, k) in synergy_bonuses.keys()
) + jm.sum(base_revenues[i] * x[i, j] for i in I for j in J)
# --- 6. 制約 ---
problem += problem.Constraint(
"parcel_assign", [jm.sum(x[i, j] for j in J) == 1 for i in I]
)
problem += problem.Constraint(
"truck_capacity",
[jm.sum(weights[i] * x[i, j] for i in I) <= capacities[j] for j in J],
)
これを Jupyter でのプレビュー(が使うLaTeXをそっくりそのまま Zenn にコピペして読めるようにしたもの)が次です:
コードと見比べてみると雰囲気がわかるのではないでしょうか。
percels_data = [1, 2, 3, 4, 5, 6, 7, 8] # I に相当
trucks_data = ["Truck A", "Truck B", "Truck C"] # J に相当
synergies_data = { (1, 3): 25, (2, 5): 30, (2, 6): 20, (4, 8): 40, (5, 7): 22 }
r_data = {1: 50, 2: 75, 3: 40, 4: 80, 5: 60, 6: 65, 7: 35, 8: 90}
weight_data = {1: 35, 2: 45, 3: 25, 4: 50, 5: 30, 6: 40, 7: 20, 8: 55}
capacity_data = {"Truck A": 100, "Truck B": 120, "Truck C": 80}
data = {
"I": percels_data,
"J": trucks_data,
"w": weight_data,
"r": r_data,
"C": capacity_data,
"s": synergies_data,
}
compiler = jm.Compiler.from_problem(problem, data)
instance = compiler.eval_problem(problem)
ラベル型については現状等値性の判定しか(意図的に)許容していませんが、もし要望が強ければ、範囲を(歯ぬけでもよい)自然数に限定した上で順序の比較ができるような、より特化されたラベル型を導入することも将来的にはありえます。
この他にも、Placeholderがタプルを要素に持てるようになり、グラフがタプルの一次元配列として表現できるようになり、専用のスマートコンストラクタも提供されるなどしています。
たとえば、以下のようにして、有向グラフの辺に沿った和を取るようなモデルが定義できます:
problem = jm.Problem("SumAlongEdgeSet", sense=jm.ProblemSense.MINIMIZE)
V = problem.Natural("V")
E = problem.Graph("E", dtype=jm.DataType.NATURAL) # デフォルトでは頂点は自然数
# Placeholderを直につかって次のようにも書ける:
# E = problem.Placeholder("E", dtype=Tuple[jm.DataType.NATURAL, jm.DataType.NATURAL], ndim=1)
x = problem.BinaryVar("x", shape=(V,))
jm.map(lambda i, j: x[i] * x[j], E).sum()
この他にも、値の部分集合を表すSet型など、表現力を向上させる様々なデータ構造を提供する予定です。
まとめ:JijModeling 2 はここが変わります!
JijModeling 2 の機能に関して簡単に予告させて頂きました。他にも盛り沢山の変更が予定されていますが、本稿で取り上げたのは以下の要素でした:
- 数式の記法がより直感的に
- 特に、不評であったElementが廃止されます!
- デコレータAPIを使うと、内包表記などより「Pyhonっぽい」記法が可能になります!
- 決定変数・プレースホルダ等のパラメータはProblemにメタデータが保存されるように
-
jm.BinaryVarなどではなく、数理モデルproblemに対してproblem.BinaryVarなどとして作成するようになります - 変数の情報が直接取得できるようになり、連続緩和などの問題の変形に構文木のトラバースが不要になりました。
-
- 配列が第一級の値に
- numpy の記法が一部採用され、より統一的で直感的な配列演算が可能になりました!
- 型システムの導入
- 問題のコンパイル前により多くのロジックエラーを検出できるようになりました!
- 型を細分するためのスマートコンストラクタも提供しており、コードの意味も明瞭になります。
- 辞書やラベル、タプルなど高級なデータ構造の提供
- グラフの表現にタプルの配列を用いたり、連続していないキーを持つデータの族を辞書を用いて表現できます。
- Jagged Array の用例は縮小していくものと考えています
より詳細な変更点や、JijModeling 1系統からの移行方法については、以下のドキュメントを御参照ください!
-
移行ガイド&チートシート(他のドキュメントも鋭意準備中です!)
-
API Documentation
現在 JijModeling 2 は2026年1月頃の正式リリースを目指して開発のラストスパートに突入したところです。
興味を持たれた方は現在公開されているプレリリース版をぜひ触っていただいて、改善要求やバグでは?と思われる挙動などがあれば、ぜひ公式の Discord の方で御気軽にフィードバックを頂ければ幸いです!
宣伝
Jijでは各ポジションを積極的に採用しています!
現在の募集職種は、以下リンクよりご覧いただけます。
カジュアル面談からのスタートも大歓迎ですので、お気軽にご連絡ください。
-
numpyのブロードキャストでは、たとえば
(N, M)配列とN配列の掛け算なども自動的にシェイプを合わせて計算してくれますが、この挙動は人間には予想しづらいところがあるため、JijModeling 2では現状意図的にサポートしない方針にしています。 ↩︎
Discussion