OpenBI Docs Buy

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]
)
ProductPlantMarginHours
PilsBergen120.01
PilsOslo110.012
PilsTrondheim100.011
IPABergen210.02

The first 4 of its 12 rows.

EVALUATE SELECTCOLUMNS ( Plants, "Plant", Plants[Plant], "Hours", Plants[Bottling hours] )
PlantHours
Bergen1,600
Oslo2,000
Trondheim900
EVALUATE SELECTCOLUMNS ( Demand, "Product", Demand[Product], "Liters", Demand[Liters] )
ProductLiters
Pils150,000
IPA60,000
Cider50,000
Lettøl40,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
ProductPlantMarginHoursLiters
PilsBergen120.01150,000
PilsOslo110.0120
PilsTrondheim100.0110
IPABergen210.025,000.00
IPAOslo19.50.01814,090.91
IPATrondheim20.50.02240,909.09
CiderBergen160.0150
CiderOslo150.01450,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
ConstraintLimitUsedBoundSlackShadow priceBinding
Bottling hours · BergenBottling hours1,6001,600075True
Bottling hours · OsloBottling hours1,3542,0006460False
Bottling hours · TrondheimBottling hours900900045.45True
Demand · PilsDemand150,000150,000011.25True
Demand · IPADemand60,00060,000019.5True
Demand · CiderDemand50,00050,000015True
Demand · LettølDemand40,00040,00007.5True

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
ScenarioMarginvs the planStatusChanges
The plan4,068,409.090optimal
Bergen +10 %4,080,409.0912,000optimalBergen hours = 1,760
IPA demand +20 %4,302,409.09234,000optimalIPA 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]
PlaceKindCratesLatLon
Depot AlnabruDepot059.9310.85
Shop BjørvikaShop1659.910.76
Shop FrognerShop1259.9210.71
Shop GrønlandShop1259.9110.76

The first 4 of its 16 rows.

EVALUATE SELECTCOLUMNS ( Distances, "From", Distances[From], "To", Distances[To], "Meters", Distances[Meters] )
FromToMeters
Depot AlnabruShop Grünerløkka8,800
Depot AlnabruShop Majorstuen15,800
Depot AlnabruShop Frogner14,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).

Last updated · OpenBI Studio 0.3.78