AbstractModel, ConcreteModel, DataPortal and Problem dumps with Pyomo
And flex your muscles!
In the previous article we solved our first problem with Pyomo and SCIP. Today we solve a second problem. We introduce the concepts of AbstractModel
, ConcreteModel
and DataPortal
. We also learn how to export a Problem into a text file for debugging.
The Truck Allocation Problem
We run a generalist product supplier business that sells a variety of products worldwide. Every day a truck comes to the warehouse to load the items that must be delivered to retailers. Usually, the number total number of items to be shipped exceed truck's capacity. Therefore we must decide which items we load into the truck and which items stay on the warehouse until the next truck.
Retailers do not pay in advance, meaning that we only get paid for the products that we actually send to them. Hence, we want to maximize the profits of all items that we ship with the truck.
As we already mentioned in the first article, a well defined Mathematical Programming Problem packs all the information that we need to understand and solve this problem. The following Pyomo implementation should be clear enough.
doc
, docstrings
, variable names
and comments
are crucial to understand the code and the Problem itself. They are the lighthouse for code readers and maintainers. Stick this in your mind.from pyomo.environ import AbstractModel
model = AbstractModel(name='truck')
Sets and Parameters
from pyomo.environ import (
NonNegativeIntegers,
NonNegativeReals,
Param,
Set,
)
model.products = Set(
name='products',
doc='Set of products to load into the truck.',
)
model.stock = Param(
model.products, # One parameter value for each element in products set
name='stock',
doc='Total number of items requested of each product',
domain=NonNegativeIntegers,
)
model.profits = Param(
model.products, # One parameter value for each element in products set
name='profits',
doc=(
'Profits obtained for each item that we load into the truck. '
'Units are €/item'
),
domain=NonNegativeReals,
)
model.volume = Param(
model.products, # One parameter value for each element in products set
name='volume',
doc='Volume of each product in cubic meters',
domain=NonNegativeReals,
)
model.truck_volume = Param( # Just one parameter
name='truck_volume',
doc='The volume of the truck in cubic meters',
domain=NonNegativeReals,
)
Variables
from typing import Tuple
from pyomo.environ import AbstractModel, NonNegativeIntegers, Var
def number_of_items_bounds(
model: AbstractModel,
product: str,
) -> Tuple[int, int]:
"""Get the lower bound and upper bound of variable stock."""
lower_bound = 0
upper_bound = model.stock[product]
return lower_bound, upper_bound
model.number_of_items = Var(
model.products, # One variable for each element in products set
name='number_of_items',
doc='Number of items to load into the truck',
domain=NonNegativeIntegers,
bounds=number_of_items_bounds,
)
We define a variable
that takes non negative nteger
values (0, 1, 2, ...). The number of items to ship is upper bounded by the total number of items to be sent.
The argument bounds
only admits a callable
that return two floats. If you need to bound a variable with a Pyomo expression
, you must use a constraint
.
Constraints
from typing import Tuple
from pyomo.environ import AbstractModel, Constraint
from pyomo.core.expr.relational_expr import InequalityExpression
def constraint_volume_of_items_cannot_exceed_truck_volume(
model: AbstractModel,
) -> InequalityExpression:
"""The volume of items in the container cannot exceed truck volume."""
shipped_volume = sum(
model.number_of_items[product] * model.volume[product]
for product in model.products
)
return shipped_volume <= model.truck_volume
# Only one constraint.
model.volume_of_items_cannot_exceed_truck_volume = Constraint(
name='volume_of_items_cannot_exceed_truck_volume',
doc=constraint_volume_of_items_cannot_exceed_truck_volume.__doc__,
rule=constraint_volume_of_items_cannot_exceed_truck_volume,
)
Objective
Thanks to the function name, the objective is crystal clear. We want to maximize
the shipped_profits
.
from pyomo.environ import AbstractModel, Objective, maximize
from pyomo.core.expr.numeric_expr import LinearExpression
def shipped_profits(model: AbstractModel) -> LinearExpression:
"""Sum the profits of all items that we load into the truck."""
shipped_profits = sum(
model.number_of_items[product] * model.profits[product]
for product in model.products
)
return shipped_profits
model.objective_function = Objective(rule=shipped_profits ,sense=maximize)
Instantiate a ConcreteModel
Mathematics build algorithms that solve Abstract Problems theoretically. This problem we're solving here is known as the Knapsack Problem, and it is already solved mathematically for any set and parameters that you choose.
In Pyomo, an AbstractModel
is a template with no data. Still the values of the sets and parameters are not defined. The bounds of the variables and the actual expression of the constraint is not defined neither. Thus, a Solver cannot solve instances of AbstractModel
because they have no data.
The Solver works with instances of ConcreteModel
. Think of a ConcreteModel
an AbstractModel
(the templated) filled with actual data. Eventually, the Solver also stores the optimal solution (and other non-optimal solutions as well) into the ConcreteModel
.
One way to load data
with Pyomo is by using a DataPortal
. In this example the DataPortal
loads the file data.json
containing the values of Sets
and parameters
. Here's the file.
# File: data.json
{
"products": ["toys", "treadmill", "locker", "costumes"],
"stock": {
"toys": 15,
"treadmill": 10,
"locker": 5,
"costumes": 20
},
"profits": {
"toys": 12,
"treadmill": 95.0,
"locker": 200,
"costumes": 40.0
},
"volume": {
"toys": 0.5,
"treadmill": 3.4,
"locker": 7.2,
"costumes": 1.4
},
"truck_volume": 50.0
}
Data seems (and is) unrealistic. Bear with that. It has been crafted specifically to generate an interesting discussion later on.
from typing import Tuple
from pyomo.environ import ConcreteModel, DataPortal, SolverFactor
# Load data from json file. Pyomo admits loading data in
# many formats. Here are more examples
# https://pyomo.readthedocs.io/en/stable/working_abstractmodels/data/dataportals.html
data = DataPortal(filename='data.json')
# Get an instance of the problem with data.
instance: ConcreteModel = model.create_instance(
name='Small container',
data=data,
)
# Get the SCIP solver
solver = SolverFactory('scip')
# Finally, solve the problem!
solution = solver.solve(
instance,
tee=True, # Log the progress of the execution of the Solver
)
Notice that the separation of AbstractModel
and ConcreteModel
is really convenient. We can define just one AbstractModel
, but solve many instances ConcreteModel
instances with different data each.
Report results
from pyomo.environ import value
profits = value(instance.objective_function)
print(f'Profits are {profits}€')
for product in instance.products:
number_of_items = round(instance.number_of_items[product].value)
volume_of_an_item = instance.volume[product]
volume_of_items = volume_of_an_item * number_of_items
print(
f'Put {number_of_items} units of {product!r}. '
f'This occupies {volume_of_items} cubic meters'
)
Notice that we read results from the ConcreteModelinstance
. Try to execute profits = value(model.objective_function)
and Pyomo will warn you that model
does not hold any data, so no value
can be computed.
The output of the Solver
We'll use the same environment we installed in the previous article.
Run this file. The solver shows logs of the whole process to help us understand the complexity of the problem and the execution process. For now, let's focus on the solution of the problem. In subsequent articles we'll understand logs in detail.
Summary of the problem
Near the beginning there's a summary of our problem.
original problem has 4 variables (0 bin, 4 int, 0 impl, 0 cont) and 1 constraints
The problem has 4 integer
variables, one for each product, and just one constraint.
For completeness, bin
refers to binary
(as we saw with the Rooks Problem), cont
refers to continuous
and impl
refers to implications
(haven't seen any examples yet).
The optimal solution
Near the end there's the solver veredict. It found 12 solutions in 0.00
seconds. One of them is the optimal solution
.
SCIP Status : problem is solved [optimal solution found]
Solving Time (sec) : 0.00
<line omited>
Primal Bound : +1.41000000000000e+03 (12 solutions)
<lines omited>
Profits are 1410.0€
Load 0 unit of 'toys'. This occupies 0.0 cubic meters
Load 6 unit of 'treadmill'. This occupies 20.4 cubic meters
Load 1 unit of 'locker'. This occupies 7.2 cubic meters
Load 16 unit of 'costumes'. This occupies 22.4 cubic meters
Few things to consider:
It found 12 solutions. This means, 12 valid item arrangements in the truck. Finally, the solver asserts that (at least) one of them is the optimal solution to the Problem.
Notice that we don't reach the
stock
limit of any product. Also, the volume of all items that we ship (50
) does not exceed the volume of the truck (50
). The Solver did a great job at filling every corner of the truck.The
locker
is the product with higher profits per item. However, we're not shipping all units that we can, but just one.💡Can you think why this happens?💡Try adding a constraint so that the number of lockers to load into the truck is two. The new solution won't as good, meaning that the profits will be lower or equal!
Debugging our implementation
With larger and more complex AbstractModel
implementations we may need to double check if we typed our Sets
, parameters
, variables
and constraints
are correctly. We can export a ConcreteModel
into a text file. Use it often, it is life-saver.
# If the instance has been already solved, the dump also contains
# the values of the optimal solution.
with open('concrete_model_dump.txt', 'wt') as f:
instance.pprint(f)
Here's the file
doc
arguments and docstrings
are!1 Set Declarations
products : Set of products to load into the truck.
Size=1, Index=None, Ordered=Insertion
Key : Dimen : Domain : Size : Members
None : 1 : Any : 4 : {'toys', 'treadmill', 'locker', 'costumes'}
4 Param Declarations
profits : Profits obtained for each item that we load into the truck. Units are €/item
Size=4, Index=products, Domain=NonNegativeReals, Default=None, Mutable=False
Key : Value
costumes : 40.0
locker : 200
toys : 12
treadmill : 95.0
stock : Total number of items requested of each product
Size=4, Index=products, Domain=NonNegativeIntegers, Default=None, Mutable=False
Key : Value
costumes : 20
locker : 5
toys : 15
treadmill : 10
truck_volume : The volume of the truck in cubic meters
Size=1, Index=None, Domain=NonNegativeReals, Default=None, Mutable=False
Key : Value
None : 50.0
volume : Volume of each product in cubic meters
Size=4, Index=products, Domain=NonNegativeReals, Default=None, Mutable=False
Key : Value
costumes : 1.4
locker : 7.2
toys : 0.5
treadmill : 3.4
1 Var Declarations
number_of_items : Number of items to load into the truck
Size=4, Index=products
Key : Lower : Value : Upper : Fixed : Stale : Domain
costumes : 0 : 16.0 : 20 : False : False : NonNegativeIntegers
locker : 0 : 1.0000000000000053 : 5 : False : False : NonNegativeIntegers
toys : 0 : 0.0 : 15 : False : False : NonNegativeIntegers
treadmill : 0 : 5.999999999999989 : 10 : False : False : NonNegativeIntegers
1 Objective Declarations
objective_function : Size=1, Index=None, Active=True
Key : Active : Sense : Expression
None : True : maximize : 12*number_of_items[toys] + 95.0*number_of_items[treadmill] + 200*number_of_items[locker] + 40.0*number_of_items[costumes]
1 Constraint Declarations
volume_of_items_cannot_exceed_truck_volume : The volume of items in the container cannot exceed truck volume.
Size=1, Index=None, Active=True
Key : Lower : Body : Upper : Active
None : -Inf : 0.5*number_of_items[toys] + 3.4*number_of_items[treadmill] + 7.2*number_of_items[locker] + 1.4*number_of_items[costumes] : 50.0 : True
8 Declarations: products stock profits volume truck_volume number_of_items volume_of_items_cannot_exceed_truck_volume objective_function
This dump also offers valuable information. For instance, notice that the column Body
of constraint volume_of_items_cannot_exceed_truck_volume
computes the volume of all the items that we load into the truck. We can use Pyomo to get the expression evaluated with the optimal solution.
from pyomo.environ import value
volume_of_all_items_loaded_into_the_truck = (
value(instance.volume_of_items_cannot_exceed_truck_volume.body)
)