Solver Max logo

31 October 2022

OR-Tools

In this article we continue the Python Production mix series. Specifically, we build Model 8 using the OR-Tools library.

OR-Tools is a project from Google. The library is freely available, the code is open source, and it is widely used.

Our objective is to compare a model built using OR-Tools with the same model built using Pyomo.

Articles in this series

Articles in the Python Production mix series:

Download the models

The model is available on GitHub.

Formulation for Model 8

For this model, we're using the same general formulation that we used in Model 2.

Model 8 Python code

Import dependencies

The first task is to import the libraries that are needed for our program. As shown in Figure 1, we import the linear solver part of the OR-Tools library, which we've previously installed, along with some other libraries that we'll use.

Figure 1. Import dependencies
from ortools.linear_solver import pywraplp
import pandas as pd
import os.path
import json

Data file

The data for Model 8 is shown in Figure 2. We're using the json format that we used in some previous models. We also specify the Google Linear Optimization Package (GLOP) solver, which is an open source linear programming solver created by Google.

Figure 2. External data file, productiondata8.json
{
    "Name": "Boutique pottery shop - Model 8",
    "Hours": 250,
    "kg": 500,
    "SalesLimit": 0,
    "Coefficients": {
        "Discs": {"People":  12.50, "Materials":  18.00, "Sales":   -2.00, "Margin":  80.00},
        "Orbs":  {"People":  10.00, "Materials":  30.00, "Sales":    1.00, "Margin": 200.00}
    },
    "VarInitial": 0,
    "VarLBounds": 0,
    "VarUBounds": 100,
    "Engine": "GLOP",
    "TimeLimit": 10
}

Get data

We import the data from the json file using the code shown in Figure 3. This code is the same as the code we used for previous json files, apart from the filename.

Figure 3. Get data
DataFilename = os.path.join('.', 'productiondata8.json')
with open(DataFilename, 'r') as f:
    Data = json.load(f)

Declarations

As shown in Figure 4, we create the model and assign various data to the Model. This code style differs from most OR-Tools examples, though it is very similar to our previous models in this series.

Figure 4. Declarations
Model = pywraplp.Solver.CreateSolver(Data['Engine'])

Model.Name = Data['Name']
Model.Hours = Data['Hours']
Model.kg = Data['kg']
Model.VarInitial = Data['VarInitial']   # Not used
Model.VarLBounds = Data['VarLBounds']
Model.VarUBounds = Data['VarUBounds']
Model.Engine = Data['Engine']
Model.TimeLimit = Data['TimeLimit']

Model.SalesLimit = Data['SalesLimit']
Coefficients = Data['Coefficients']
Model.Products = list(Coefficients.keys())
Model.Production = {}

Model.People = {}
Model.Materials = {}
Model.Sales = {}
Model.Margin = {}

for p in Model.Products:    
    Model.People[p] = Coefficients[p]['People']
    Model.Materials[p] = Coefficients[p]['Materials']
    Model.Sales[p] = Coefficients[p]['Sales']
    Model.Margin[p] = Coefficients[p]['Margin']

Define the model

The model definition, as shown in Figure 5, starts with defining the variables. We then define the constraints and objective function in a straightforward manner. Note that we specify the optimization direction using Model.Maximize.

We've adopted a simple style in this example, though we could use the def structure that we've used in some previous models. When developing more complex models (which we'll do later, when exploring OR-Tool's constraint optimization capabilities), it can be useful to adopt a def structure, to make the model more flexible and easier to understand.

Note that we provide a name at the end of each constraint. This name is used in the model output, if we ask OR-Tools to output the model in LP format (which we do below, if there is no solution).

Figure 5. Define the model
for p in Model.Products:
    Model.Production[p] = Model.NumVar(Model.VarLBounds, Model.VarUBounds, p)

Model.PeopleHours = Model.Add(sum(Model.People[p] * Model.Production[p] for p in Model.Products) <= Model.Hours, 'PeopleHours')
Model.MaterialUsage = Model.Add(sum(Model.Materials[p] * Model.Production[p] for p in Model.Products) <= Model.kg, 'MaterialUsage')
Model.SalesRelationship = Model.Add(sum(Model.Sales[p] * Model.Production[p] for p in Model.Products) <= Model.SalesLimit, 'SalesRelationship')

Model.TotalMargin = sum(Model.Margin[p] * Model.Production[p] for p in Model.Products)
Model.Maximize(Model.TotalMargin)

Solve model

As shown in Figure 6, we define the option to specify a time limit (in seconds) for GLOP. We then solve the model and record the solve status.

Figure 6. Solve model
Model.set_time_limit(Model.TimeLimit)
Status = Model.Solve()

Process results

The code for processing the solver result, as shown in Figure 7, is similar to the code for previous models.

Note that GLOP can return a variety of solve status codes.

Figure 7. Process results
WriteSolution = False
Optimal = False

if Status == pywraplp.Solver.OPTIMAL:
    Optimal = True
    WriteSolution = True
    StatusText = 'Optimal'
elif (Status == pywraplp.Solver.INFEASIBLE):
    StatusText = 'Infeasible'
elif (Status == pywraplp.Solver.UNBOUNDED):
    StatusText = 'Unbounded'
elif (Status == pywraplp.Solver.ABNORMAL): 
    StatusText = 'Abnormal'
elif (Status == pywraplp.Solver.NOT_SOLVED): 
    StatusText = 'Not solved'

Write output

The code for writing the output, as shown in Figure 8, is very similar to our previous models. The main difference is the syntax for extracting the slack and dual values from the GLOP solution.

Figure 8. Write output
print(Model.Name, '\n')
print('Status:', StatusText)
print('Solver:', Model.Engine, '\n')

if WriteSolution:
    print(f"Total margin = ${Model.Objective().Value():,.2f}\n")
    pd.options.display.float_format = "{:,.4f}".format
    ProductResults = pd.DataFrame()
    for p in Model.Products:
        ProductResults.loc[p, 'Production'] = Model.Production[p].solution_value()
    display(ProductResults)

    ConstraintStatus = pd.DataFrame(columns=['Slack', 'Dual'])
    activities = Model.ComputeConstraintActivities()
    for i, constraint in enumerate(Model.constraints()):
        ConstraintStatus.loc[constraint.name()] = [constraint.ub() - activities[constraint.index()], constraint.dual_value()]
    display(ConstraintStatus)
else:
    print('No solution loaded\n')
    print('Model:')
    print(Model.ExportModelAsLpFormat(False).replace('\\', ' '))

When we find an optimal solution, the output is shown in Figure 9. This output is similar to previous models, except for the model's name.

Figure 9. Solution
    Boutique pottery shop - Model 8 

    Status: Optimal
    Solver: GLOP 

    Total margin = $3,076.92

    Production
    Discs      6.4103
    Orbs      12.8205 

    Slack    Dual
    PeopleHours         41.6667 -0.0000
    MaterialUsage       -0.0000  6.1538
    SalesRelationship   -0.0000 15.3846
  

Evaluation of this model

This model is very similar to our Pyomo models. Although the syntax of the two libraries is somewhat different, the general structure of the model definitions and solution process is familiar.

Despite the similarities, we tend to prefer Pyomo for solving linear programs, simply because Pyomo offers access to a wider range of solvers for solving a greater variety of model types, and OR-Tools offers no significant advantage over Pyomo for this type of problem.

Although OR-Tools can solve our Production mix linear programming model, the real strength of OR-Tools is in its constraint programming capability. That is, OR-Tools has packages designed to solve specific types of models that are otherwise difficult to solve, including assignment, routing, scheduling, and bin packing problems. We'll explore those capabilities in later articles.

Next steps

In subsequent articles we'll repeat the model implementation using our other selected libraries. Next on the list is Gekko, which has a focus on machine learning along with optimization of mixed-integer and differential algebraic equations.

Conclusion

In this article we built the Production mix model using the OR-Tools library. Compared with the Pyomo models, the code is quite similar. OR-Tools is a capable modelling library that is easy to use. However, since it offers no significant advantage compared with Pyomo, we tend to prefer Pyomo over OR-Tools for this type of model.

In the next article, we'll build the Production mix model using Gekko.

If you would like to know more about this model, or you want help with your own models, then please contact us.