Gurobi Fsharp Library
The purpose of this library is to create an idiomatic F# wrapper around the Gurobi .NET library. The reason for this is that F# is a functional-first language and the composition model favors functions over methods. F# is perfectly capable of working with objects and their methods but I prefer function composition so I decided to create my own wrapper around the Gurobi object model to enable that.
Gurobi provides a Python library for their solver which allows for the simple and elegant building of models. I am purposefully emulating the style of that library. I wanted to bring the beauty of that library to F#. Hats off to the Gurobi team for providing an excellent blueprint to emulate.
Note This is a work in progress. Support will expand as I use the tool more and need to integrate additional capabilities.
The following shows an example of a network flow problem provided by Gurobi and modeled in Python. The full formulation can be found here. In this example I am just comparing and contrasting the Python and F# constraint formulation methods.
Disclaimer: All Python code is copyrighted by Gurobi Optimization, LLC
Creating a model
In Python the creation of the model and decision variables is quite straightforward.
# Copyright 2018, Gurobi Optimization, LLC # Create optimization model m = Model('netflow') # Create variables flow = m.addVars(commodities, arcs, obj=cost, name="flow")
In F# we have a similar syntax but instead of
flow being a
Dictionary of decision variables, we produce a
Map<string list, GRBDecVar> which is essentially the same for our purposes.
// Create a new instance of the Gurobi Environment object // to host models let env = Environment.create // Create a new model with the environment variable let m = Model.create env "netflow" // Create a Map of decision variables for the model // addVarsForMap <model> <lower bound> <upper bound> <type> <input Map> let flow = Model.addVarsForMap m 0.0 INF CONTINUOUS costs
Instead of using the methods on the object, functions have been provided which operate on the values that are passed in. This is more idiomatic for F#. The
Model module in the library hosts all of the functions for working with objects of type
Model.adddVarsForMap function takes a
Map<string list, float> and produces a
Map<string list, GRBDecVar> for the modeler to work with. This is similar to how the Python tuples are working in the
gurobipy library. Instead of indexing into a Python dictionary with
tuples, F# uses a
string list as the index.
gurobipy library offers a succinct way of expressing a whole set of constraints by using generators. There is additional magic going on under the hood though that may not be obvious at first. The following method generates a set of constraints for each element in
arcs but also creates a meaningful constraint name. The prefix for the constraint name is the last argument of the method (
"capacity" in this instance).
# Arc capacity constraints capacityConstraints = m.addConstrs( (flow.sum('*',i,j) <= capacity[i,j] for i,j in arcs), "capacity")
There is also special sauce occuring in the
flow is a dictionary which is indexed by a 3 element tuple. What this
sum() method is doing is summing across all elements in the dictionary which fit the pattern. The
* symbol is a wildcard and will match against any element. This is a powerful way to sum across dimensions of the optimization model.
In F# we can do something similar but instead of having a generator we pass in a lambda to create the constraints.
let capacityConstraints = Model.addConstrs m "capacity" arcs (fun [i; j] -> (sum flow ["*"; i; j] <== capacity.[[i; j]]))
Model.addConstrs takes a
model object as its first argument (
m in this case), the prefix for what the constraints are going to be named (
"capacity" in this case), and the set of indices the constraints will be created over,
arcs in this case. The key point is that the types of the indices must match the input type of the lambda.
addConstrs function will iterate through each of the indices in the set, create a constraint from the lambda that was passed, and name the constraint appropriatly. If the first element of the
arcs set was
["Detroit"; "Boston"] then the name of the first constraint would be
capacity_Detroit_Boston. This helps the modeler by maintaining a consistent naming scheme for the constraints in the model.