5 September 2022 (1,891 words)
In this article we continue the Python Production mix series, using the Pyomo library. Specifically, we build Model 5, which changes Model 4 to:
- Define the constraints and objective function using
- Output the slack values and dual prices (also known as shadow prices) for each constraint.
These changes give us more control over how the model is defined and provide more information about the solution.
Articles in this series
Articles in the Python Production mix series:
- Python optimization Rosetta Stone
- Production mix - Model 1, Pyomo concrete
- Production mix - Model 2, Pyomo separate data
- Production mix - Model 3, Pyomo external data
- Production mix - Model 4, Pyomo json file
- Production mix - Model 5, Pyomo using def
- Production mix - Model 6, Pyomo abstract
- Production mix - Model 7, PuLP
- Production mix - Model 8, OR Tools
- Production mix - Model 9, Gekko
- Production mix - Model 10, CVXPY
- Production mix - Model 11, SciPy
- Production mix - Conclusions
Download the model
The Python code and data for this model are available in the following files:
- Jupyter notebook: production-model-5.ipynb
- Python code: production-model-5.py
- Data: productiondata5.json
The "Jupyter notebook" file contains a formatted combination of Python code and markdown code – this file should be opened and run in Jupyter Lab. We describe setting up our Python environment, including Jupyter Lab and various Python libraries, in the article Set up a Python modelling environment.
The "Python code" file is a plain text file containing only the Python code of this model. Download this file if you have a non-Jupyter environment for running Python programs.
The "Data" file is a plain text file containing the model's data, in json format.
The model files are also available on GitHub.
Formulation for Model 5
For this model, we're using the same general formulation that we used for Model 4, as shown in Figure 1.
Model 5 Python code
The first task is to import the libraries that are needed for our program. The dependencies, as shown in Figure 2, are the same as for Model 4.
The data for Model 5, as shown in Figure 3, is identical to the data for Model 4, except for the model's name.
Note that to edit a json file in Jupyter Lab, you'll need to right-click on the file and select Open with > Editor.
To load the json file, we use the
json libraries, as shown in Figure 4. This code loads all the json file data into a single object, which we'll parse in the next section.
The declarations, as shown in Figure 5, are identical to Model 4.
Define the model
The model definition is shown in Figure 6. Our definition of the Production variables is the same as for Model 4, but the definitions of the constraints and the objective function are very different.
Specifically, we use a
def function to define each of the constraints and the objective function. In this model, each definition consists of three lines of code:
- First line: Declare a function. We need to give the function a name and pass at least the
Modelobject to the function. In more complex models we would also pass indices and possibly other objects.
- Second line: We return a rule for the constraint or objective function. The rules are the same as the expressions we defined in Model 4. In more complex models, this line would expand to multiple lines, with logic to decide what to return. For example, a common task is to skip specific instances of a constraint.
- Third line: Here we call the function, indicating which rule to use and whether we're defining a
pyo.Objective. This line doesn't need to be immediately after the function definition, though it is common practice to do so. For complex models, which have long functions, it may be clearer to define the functions and then have their calls collated in a subsequent block of code.
The advantage of using
def functions is that they give us much greater control over the definitions. For all but simple models, the standard way to define constraints and the objective function in Pyomo is to use
As shown in Figure 7, the code for solving the model is almost the same as for Model 4. The exception is that we've added a line that tells the solver to collate
Model.dual prices for the constraints, which we'll print in the output.
The code for processing the solver result, as shown in Figure 8, is the same as for Model 4.
The code for writing the output, as shown in Figure 9, is almost the same as for Model 4, except that:
- We define the default data frame format to have 4 decimal places (i.e., using
pd.options.display.float_format), so we have additional precision for calculations that we do below.
- We've added a section to populate a
ConstraintStatusdata frame that contains the slack values and dual prices for each constraint.
When we find an optimal solution, the output is shown in Figure 10.
The table of slack values and dual prices can be interpreted as follows:
- lSlack is the difference between the constraint's lower bound and its value in the solution. None of our constarints have lower bounds, so the differences are infinite.
- uSlack is the difference between the constraint's upper bound and its value in the solution. For the PeopleHours constraint, uSlack = 41.6667. We can verify this value by substituting the solution's variable values into the constraint, so the left-hand side is: 12.50*6.4103 + 10.00*12.8205 = 208.3338, which is 41.6662 less than the right-hand side of 250 (give-or-take a small rounding difference). If we do a similar calculation for the MaterialsUsage and SalesRelationship constraints, the result is zero – indicating that those constraints are binding.
- The dual prices indicate the marginal change in the objective function for a unit change in a constraint's right-hand side, all else being equal. For the non-binding PeopleHours constraint, the dual price is zero because a change in the right-hand side value has no impact on the solution's objective function value. The other two constraints are binding, so their dual prices are non-zero. For example, if we change the available materials to 501 kg, and re-solve the model, then the objective function increases by 6.16 to 3,083.08 (again, allowing for a small rounding difference). Similarly, changing the available materials to 499 kg reduces the objective function value by 6.15 to 3,070.77.
Evaluation of this model
Model 5 is our final concrete model in this series of articles.
This model contains the essential elements of a Pyomo optimization model, including importing data, creating data structures, defining the model, solving the model, processing the result, and producing outputs that display the solution and help us understand the solution.
Every model is different, so other models may need to alter or extend the design of this model. Nonetheless, this model provides a good template from which to develop other Pyomo models.
We could further extend this model, particularly by adding more error checking and data validation. For an operational model, such features may be important.
But we'll leave such details for another time. Instead, the next article will focus on an alternative implementation, using a Pyomo "abstract" model. Subsequent articles will then implement the Production mix model using other Python modelling libraries.
This article continues our exploration of optimization modelling in Python. Compared with Model 4, we define the constraints and objective function using functions, which gives us much greater control over the model definition. We also output additional information about the solution, specifically the slack values and dual prices.
In the next article, we'll look at an alternative way of defining the model, via a Pyomo abstract model.
If you would like to know more about this model, or you want help with your own models, then please contact us.