Flipsに入門してみました. ただの備忘録です. 私の個人的な趣味です.
目次
背景
- F#でOR-Toolsを使ってみた
- 何となく, F#っぽくない?ような気がした
- F#っぽい書き方が出来るライブラリは無いか?->Flipsに行きつく
※F#っぽくない?ような気がした件について.
大きく2つあります.
1つ目は, OR-Toolsでは制約式や目的関数を生成する際, model.Addという書き方をします.
個人的に, この書き方がオブジェクト指向的に感じておりまして. F#っぽくないな?と, 感じます.
2つ目は, F#で制約式を追加する際, 例えばLinearExpr.op_GreaterThanOrEqualという書き方をします.(<=のようなオペレータを使用すれば良いのでは?と思いましたが, 私では上手く行かず.)
let expr = LinearExpr.Sum(vars)
model.Add(LinearExpr.op_GreaterThanOrEqual (expr, int64 10)) |> ignore
こちらの書き方が, 個人的には少し受け入れ難いというか... F#の関数型的な書き味が変わるな?と感じました.
C#だと, 個人的に違和感がありません. C#ではオペレータがそのまま使えます.
solver.Add(1.0 * var1 <= 300.0);
solver.Add(0.5 * var1 + 0.4 * var2 <= 200.0);
Flipsとは?
先述した個人的な違和感を解消してくれそうなライブラリとして, Flipsに行きつきました.
まず, Flipsとは?公式ドキュメントによると, 以下のように書いてあります.
Flips is an F# library for modeling and solving Linear Programming (LP) and Mixed-Integer Programming (MIP) problems. It is inspired by the work of the PuLP library for Python and the excellent Gurobi Python library. It builds on the work of the outstanding Google OR-Tools library and the OPTANO library.
ざっくり日本語訳すると,
- Flipsとは, F#の為の線形計画法(LP)と混合整数計画法(MIP)をモデリング出来て求解出来るライブラリ
- PuLPやGurobiのPythonライブラリにインスパイアされた
- OR-ToolsとOPTANOを使って動く
要は, OR-ToolsとOPTANOのF#用ラッパーライブラリと思うと良いのでは?と思います.
実際にコードを写経して書き替えてみた
導入は簡単です. nugetすれば良し. .NET勢ならすぐに導入できます.
公式ドキュメントにはシンプルなフードトラックを模した数理最適化問題の求解が書かれています. ただ, 手続き的な書き方が多いと感じます.
恐らく, C#などの.NET勢やPython/Julia等のピュア最適解人材をターゲットとしていて, 関数型プログラミングにはあまり慣れていないことを想定しているのかな?と邪推します. 下衆の勘繰りと読み流してください.
私はF#っぽい書き方をしたいので, ちょっとだけ書き換えました.
ドメイン部分
Flipsを呼び出し, 商品と決定変数を生成します.
// Program.fs
namespace Core
module Program =
open Flips
open Flips.Types
/// 商品のデータ構造(ドメインモデル)
type Item = {
Name: string
Profit: float
MaxSupply: float
Weight: float
}
/// 元のデータと決定変数をペアで保持する
type ItemDecision = {
Item: Item
Decision: Decision
}
計算と可視化部分
サクッと関数を作っておきます.
visualizeSolution関数について, 個人的に面白かったのが, 通常のオブジェクト指向的な考え方なら, Solution.ObjectiveValue()やObjective.Valueなどで目的関数の値にアクセス出来るようにするはずです.
しかし, わざわざObjective.evaluate solution objectiveと, 解と目的関数を評価関数に渡しています. 関数型プログラミングでは状態を持つことが出来ないので, このような表示方法になるのだと思います.
// Program.fs
namespace Core
module Program =
open Flips
open Flips.Types
// -----------------------------------------------
// 中略
// -----------------------------------------------
/// 商品名から変数を作成する
let createContinuousDecision (itemName: string) : Decision =
Decision.createContinuous $"NumberOf{itemName}" 0.0 infinity
/// 目的関数を生成する
let createObjective (objectiveExpression: LinearExpression): Objective =
Objective.create "MaximizeRevenue" Maximize objectiveExpression
/// 供給量に関する制約式を生成する
let createSupplyConstraint (pair: ItemDecision): Constraint =
Constraint.create ($"MaxOf{pair.Item.Name}") (pair.Decision <== pair.Item.MaxSupply)
/// トラックの重さに関する制約式を生成する
let createWeightConstraint (maxTruckWeight: float) (weightExpr: LinearExpression): Constraint =
Constraint.create ("MaxWeight") (weightExpr <== maxTruckWeight)
/// 正常に解が出た場合に標準出力で表示する
let visualizeSolution (solution: Solution) (objective: Objective): unit =
let obj = Objective.evaluate solution objective
printfn $"Objective Value: %f{obj}"
solution.DecisionResults
|> Map.toList
|> List.iter (fun (dec, value) ->
let (DecisionName name) = dec.Name
printfn $"%s{name}: %f{value}"
)
// -----------------------------------------------
// 中略
// -----------------------------------------------
メイン関数
メイン関数では, これまでに作った道具たちを組み合わせていきます.
全体的にパイプライン演算子(|>)で書けていると思います.
// Program.fs
namespace Core
module Program =
open Flips
open Flips.Types
// -----------------------------------------------
// 中略
// -----------------------------------------------
[<EntryPoint>]
let main (args: string array) =
// アイテムリスト, 固定値を定義
let items: Item list = [
{Name = "Hamburger"; Profit = 1.5; MaxSupply = 300.0; Weight = 0.5}
{Name = "HotDog" ; Profit = 1.2; MaxSupply = 200.0; Weight = 0.4}
]
let maxTruckWeight = 200.0
// アイテムとその変数を生成
let itemDecisions =
items |> List.map (fun item -> {
Item=item
Decision = createContinuousDecision item.Name
})
// 目的関数を生成
let objective =
itemDecisions
|> List.sumBy (fun pair -> pair.Item.Profit * pair.Decision)
|> createObjective
// 各商品の数量に関する制約を生成
let supplyConstraints =
itemDecisions
|> List.map (fun pair ->
createSupplyConstraint pair
)
// 各商品の重さに関する制約を生成
let weightConstraint =
itemDecisions
|> List.sumBy (fun pair -> pair.Item.Weight * pair.Decision)
|> createWeightConstraint maxTruckWeight
// モデルを生成
let model =
Model.create objective
|> Model.addConstraints supplyConstraints
|> Model.addConstraint weightConstraint
// 設定ファイルを作る
let settings = {
SolverType = SolverType.CBC
MaxDuration = 10_000L
WriteLPFile = None
WriteMPSFile = None
}
// 求解して画面表示
let result = Solver.solve settings model
printfn "-- Result --"
match result with
| Optimal solution ->
visualizeSolution solution objective
| _ ->
printfn $"Unable to solve. Error: %A{result}"
0
個人的な感想
全体的にF#っぽく書ける, 面白いライブラリだと思います. ただ, 0-1変数(Boolean/Integer decision)の扱い方や応用パターンについてはもう少し調べてみたいな, と思っています.
また, 私のコードはザックリとしか書いていませんので, 本来なら解き方のコンフィグなどは外だしするべきだな, とも思っています.
読んでくれてありがとうございました.
Appendix
リンク集
-
Flips公式ドキュメント
- 情熱的に色々書いてあります
- 分枝限定法や局所探索等のリンク集もあります
- 暇な時にでも覗いてみます
-
FlipsのGitHub
- FlipsのGitHubです
- たった18人のコントリビュータで回してるってすごいな...
- Languagesは100パーセントF#, 愛がスゴイ
コードの全文
コードの全文を載せます.
// Program.fs
namespace Core
module Program =
open Flips
open Flips.Types
/// 商品のデータ構造(ドメインモデル)
type Item = {
Name: string
Profit: float
MaxSupply: float
Weight: float
}
/// 元のデータと決定変数をペアで保持する
type ItemDecision = {
Item: Item
Decision: Decision
}
/// 商品名から変数を作成する
let createContinuousDecision (itemName: string) : Decision =
Decision.createContinuous $"NumberOf{itemName}" 0.0 infinity
/// 目的関数を生成する
let createObjective (objectiveExpression: LinearExpression): Objective =
Objective.create "MaximizeRevenue" Maximize objectiveExpression
/// 供給量に関する制約式を生成する
let createSupplyConstraint (pair: ItemDecision): Constraint =
Constraint.create ($"MaxOf{pair.Item.Name}") (pair.Decision <== pair.Item.MaxSupply)
/// トラックの重さに関する制約式を生成する
let createWeightConstraint (maxTruckWeight: float) (weightExpr: LinearExpression): Constraint =
Constraint.create ("MaxWeight") (weightExpr <== maxTruckWeight)
/// 正常に解が出た場合に標準出力で表示する
let visualizeSolution (solution: Solution) (objective: Objective): unit =
let obj = Objective.evaluate solution objective
printfn $"Objective Value: %f{obj}"
solution.DecisionResults
|> Map.toList
|> List.iter (fun (dec, value) ->
let (DecisionName name) = dec.Name
printfn $"%s{name}: %f{value}"
)
[<EntryPoint>]
let main (args: string array) =
// アイテムリスト, 固定値を定義
let items: Item list = [
{Name = "Hamburger"; Profit = 1.5; MaxSupply = 300.0; Weight = 0.5}
{Name = "HotDog" ; Profit = 1.2; MaxSupply = 200.0; Weight = 0.4}
]
let maxTruckWeight = 200.0
// アイテムとその変数を生成
let itemDecisions =
items |> List.map (fun item -> {
Item=item
Decision = createContinuousDecision item.Name
})
// 目的関数を生成
let objective =
itemDecisions
|> List.sumBy (fun pair -> pair.Item.Profit * pair.Decision)
|> createObjective
// 各商品の数量に関する制約を生成
let supplyConstraints =
itemDecisions
|> List.map (fun pair ->
createSupplyConstraint pair
)
// 各商品の重さに関する制約を生成
let weightConstraint =
itemDecisions
|> List.sumBy (fun pair -> pair.Item.Weight * pair.Decision)
|> createWeightConstraint maxTruckWeight
// モデルを生成
let model =
Model.create objective
|> Model.addConstraints supplyConstraints
|> Model.addConstraint weightConstraint
// 設定ファイルを作る
let settings = {
SolverType = SolverType.CBC
MaxDuration = 10_000L
WriteLPFile = None
WriteMPSFile = None
}
// 求解して画面表示
let result = Solver.solve settings model
printfn "-- Result --"
match result with
| Optimal solution ->
visualizeSolution solution objective
| _ ->
printfn $"Unable to solve. Error: %A{result}"
0