Optimization
Plans that decide under limits: how much each plant should bottle, which shops each van visits. A notebook makes a decision per row of an OAX step, says what to make as large or as small as it can, and adds the limits; OR-Tools solves it, and the solution says what limits it in words.
The data
Example Brewery’s production plan: what a liter of each product earns and takes to bottle at each plant, each plant’s bottling hours, and each product’s demand next month.
// a decision per product and plant: what a liter earns and takes to bottle
EVALUATE
SELECTCOLUMNS (
Production,
"Product", Production[Product],
"Plant", Production[Plant],
"Margin", Production[Margin per liter],
"Hours", Production[Hours per liter]
)
| Product | Plant | Margin | Hours |
|---|---|---|---|
| Pils | Bergen | 12 | 0.01 |
| Pils | Oslo | 11 | 0.012 |
| Pils | Trondheim | 10 | 0.011 |
| IPA | Bergen | 21 | 0.02 |
The first 4 of its 12 rows.
EVALUATE SELECTCOLUMNS ( Plants, "Plant", Plants[Plant], "Hours", Plants[Bottling hours] )
| Plant | Hours |
|---|---|
| Bergen | 1,600 |
| Oslo | 2,000 |
| Trondheim | 900 |
EVALUATE SELECTCOLUMNS ( Demand, "Product", Demand[Product], "Liters", Demand[Liters] )
| Product | Liters |
|---|---|
| Pils | 150,000 |
| IPA | 60,000 |
| Cider | 50,000 |
| Lettøl | 40,000 |
The model
Optimize.Maximize (or Minimize) starts a model. Decide makes a decision for each row of a table, and By groups them, so each plant’s hours and each product’s demand is a constraint of its own. Capacities and demands are parameters: scenarios and report pages move them.
// liters per row; the margin as large as it can be; hours and demand as parameters (scenarios and reports move them)
var plan = Optimize.Maximize("Margin");
var liters = plan.Decide("Liters", rows, describe: "Liters of each product bottled at each plant next month");
plan.Objective(liters.Sum("Margin"), "margin per liter × liters");
foreach (var (plant, x) in liters.By("Plant"))
plan.Constrain("Bottling hours", x.Sum("Hours") <= plan.Parameter($"{plant} hours", plants.Rows.First(r => r.Text("Plant") == plant).Number("Hours")), per: plant);
foreach (var (product, x) in liters.By("Product"))
plan.Constrain("Demand", x.Sum() <= plan.Parameter($"{product} demand", demand.Rows.First(r => r.Text("Product") == product).Number("Liters")), per: product);
plan.Describe()
DECIDE
Liters: one per row of 12 (Product × Plant), 0 or more. Liters of each product bottled at each plant next month
TO MAXIMIZE
Margin: Σ Margin × Liters. margin per liter × liters
SUBJECT TO
Bottling hours, per Plant (3: Bergen, Oslo, Trondheim): Σ Hours × Liters ≤ {Plant} hours
Demand, per Product (4: Pils, IPA, Cider, Lettøl): Σ Liters ≤ {Product} demand
PARAMETERS
Bergen hours = 1,600
Oslo hours = 2,000
Trondheim hours = 900
Pils demand = 150,000
IPA demand = 60,000
Cider demand = 50,000
Lettøl demand = 40,000
Describe() says the model as a decision maker reads it; the Model tab shows the same.
Solve it
var solution = plan.Solve();
solution
Optimal: the best Margin there is, 4,068,409
Bottling hours · Bergen is at its limit (1,600): one more adds 75 to Margin
Bottling hours · Trondheim is at its limit (900): one more adds 45.45 to Margin
Demand · IPA is at its limit (60,000): one more adds 19.5 to Margin
Demand · Cider is at its limit (50,000): one more adds 15 to Margin
Demand · Pils is at its limit (150,000): one more adds 11.25 to Margin
Each binding limit comes with its shadow price: what one more hour or liter of it would add to the margin. One more bottling hour in Bergen is worth 75; Oslo has hours to spare, so another is worth nothing.
The plan and its limits
solution.Plan
| Product | Plant | Margin | Hours | Liters |
|---|---|---|---|---|
| Pils | Bergen | 12 | 0.01 | 150,000 |
| Pils | Oslo | 11 | 0.012 | 0 |
| Pils | Trondheim | 10 | 0.011 | 0 |
| IPA | Bergen | 21 | 0.02 | 5,000.00 |
| IPA | Oslo | 19.5 | 0.018 | 14,090.91 |
| IPA | Trondheim | 20.5 | 0.022 | 40,909.09 |
| Cider | Bergen | 16 | 0.015 | 0 |
| Cider | Oslo | 15 | 0.014 | 50,000 |
The first 8 of its 12 rows.
solution.Limits lists every constraint, what it uses of its bound, its slack and its shadow price; solution.Ranging says how far each bound can move before its shadow price changes, and solution.ReducedCosts what a decision left at zero would have to earn to be used.
// how far each limit can move before its shadow price changes
solution.Limits
| Constraint | Limit | Used | Bound | Slack | Shadow price | Binding |
|---|---|---|---|---|---|---|
| Bottling hours · Bergen | Bottling hours | 1,600 | 1,600 | 0 | 75 | True |
| Bottling hours · Oslo | Bottling hours | 1,354 | 2,000 | 646 | 0 | False |
| Bottling hours · Trondheim | Bottling hours | 900 | 900 | 0 | 45.45 | True |
| Demand · Pils | Demand | 150,000 | 150,000 | 0 | 11.25 | True |
| Demand · IPA | Demand | 60,000 | 60,000 | 0 | 19.5 | True |
| Demand · Cider | Demand | 50,000 | 50,000 | 0 | 15 | True |
| Demand · Lettøl | Demand | 40,000 | 40,000 | 0 | 7.5 | True |
Scenarios
The plan solved again with other values, and compared with it:
// the plan solved again with other values, compared with the base
solution.Scenarios(new Scenario("Bergen +10 %", ("Bergen hours", 1760)), new Scenario("IPA demand +20 %", ("IPA demand", 72000))).Compare
| Scenario | Margin | vs the plan | Status | Changes |
|---|---|---|---|---|
| The plan | 4,068,409.09 | 0 | optimal | |
| Bergen +10 % | 4,080,409.09 | 12,000 | optimal | Bergen hours = 1,760 |
| IPA demand +20 % | 4,302,409.09 | 234,000 | optimal | IPA demand = 72,000 |
Ten percent more hours in Bergen adds 12,000; twenty percent more IPA demand adds 234,000. solution.Sweep solves for a list of values of one parameter, and the Scenarios tab keeps them beside the plan.
Routes
Routing.Plan routes vehicles over a square of distances: three vans from the depot, 70 crates each, driving as little as they can, on the shops’ driving distances in Oslo.
EVALUATE
SELECTCOLUMNS ( Places, "Place", Places[Place], "Kind", Places[Kind], "Crates", Places[Crates], "Lat", Places[Lat], "Lon", Places[Lon] )
ORDER BY [Kind], [Place]
| Place | Kind | Crates | Lat | Lon |
|---|---|---|---|---|
| Depot Alnabru | Depot | 0 | 59.93 | 10.85 |
| Shop Bjørvika | Shop | 16 | 59.9 | 10.76 |
| Shop Frogner | Shop | 12 | 59.92 | 10.71 |
| Shop Grønland | Shop | 12 | 59.91 | 10.76 |
The first 4 of its 16 rows.
EVALUATE SELECTCOLUMNS ( Distances, "From", Distances[From], "To", Distances[To], "Meters", Distances[Meters] )
| From | To | Meters |
|---|---|---|
| Depot Alnabru | Shop Grünerløkka | 8,800 |
| Depot Alnabru | Shop Majorstuen | 15,800 |
| Depot Alnabru | Shop Frogner | 14,500 |
The first 3 of its 240 rows.
// three vans from the depot (the first place), 70 crates each, as little driving as they can
var names = places.Rows.Select(r => r.Text("Place")!).ToList();
var meters = new long[names.Count, names.Count];
foreach (var r in distances.Rows)
meters[names.IndexOf(r.Text("From")!), names.IndexOf(r.Text("To")!)] = (long)r.Number("Meters");
var crates = places["Crates"].ToNumbers();
var routes = Routing.Plan(names, meters).Vehicles(3, depot: 0).Capacity(crates, [70, 70, 70], "Crates")
.Coordinates(places)
.Solve(seconds: 5);
routes
Routes for 3 of 3 vehicles: 65,700 in all, the longest 26,900, 178 carried
Vehicle 1: Depot Alnabru → Shop Storo → Shop Nydalen → Shop Sagene → Shop Torshov → Shop Grünerløkka → Depot Alnabru · 19,000 · load 64
Vehicle 2: Depot Alnabru → Shop Ullevål → Shop Majorstuen → Shop Skøyen → Shop Frogner → Shop St. Hanshaugen → Depot Alnabru · 26,900 · load 56
Vehicle 3: Depot Alnabru → Shop Vålerenga → Shop Kampen → Shop Tøyen → Shop Grønland → Shop Bjørvika → Depot Alnabru · 19,800 · load 58
With the places’ coordinates, the Routes tab draws them on a map. Time windows, pickups and deliveries, a maximum distance per vehicle and several depots are rules of the same plan. A search with a time limit can find a different route of the same length on another run.
More
Integer and yes/no decisions (integer: true, Choose), robust plans over uncertain numbers, plans over several periods and stochastic two-stage plans have recipes in the analytics catalog. OR-Tools’ own .NET classes are there too, by their namespaces: CP-SAT for schedules, flows, assignment and knapsacks. Every member is in the Optimization reference.
Keep it
Results.Save("Production plan", solution) keeps the plan as a draft version, with its model, its limits and how far they hold. Before it is approved, Review as a page in its Versions tab writes a review page of the plan exactly as saved. Once it is in use, the plan’s rows are a table of the model, and a what-if page solves it again as sliders move (see Operations Research).