Incremental modeling with decision optimization

This tutorial includes everything you need to set up decision optimization engines, build a mathematical programming model, then incrementally modify it. You will learn how to:

  • change coefficients in an expression
  • add terms in an expression
  • modify constraints and variables bounds
  • remove/add constraints
  • play with relaxations

When you finish this tutorial, you'll have a foundational knowledge of Prescriptive Analytics.

This notebook is part of Prescriptive Analytics for Python

It requires either an installation of CPLEX Optimizers or it can be run on IBM Watson Studio Cloud (Sign up for a free IBM Cloud account and you can start using Watson Studio Cloud right away).

Table of contents:


Describe the business problem: Telephone production

A possible descriptive model of the telephone production problem is as follows:

  • Decision variables:
    • Number of desk phones produced (DeskProduction)
    • Number of cellular phones produced (CellProduction) Objective: Maximize profit
  • Constraints:
    • The DeskProduction should be greater than or equal to 100.
    • The CellProduction should be greater than or equal to 100.
    • The assembly time for DeskProduction plus the assembly time for CellProduction should not exceed 400 hours.
    • The painting time for DeskProduction plus the painting time for CellProduction should not exceed 490 hours.

This is a type of discrete optimization problem that can be solved by using either Integer Programming (IP) or Constraint Programming (CP).

Integer Programming is the class of problems defined as the optimization of a linear function, subject to linear constraints over integer variables.

Constraint Programming problems generally have discrete decision variables, but the constraints can be logical, and the arithmetic expressions are not restricted to being linear.

For the purposes of this tutorial, we will illustrate a solution with mathematical programming (MP).

How decision optimization can help

  • Prescriptive analytics (decision optimization) technology recommends actions that are based on desired outcomes. It takes into account specific scenarios, resources, and knowledge of past and current events. With this insight, your organization can make better decisions and have greater control of business outcomes.

  • Prescriptive analytics is the next step on the path to insight-based actions. It creates value through synergy with predictive analytics, which analyzes data to predict future outcomes.

  • Prescriptive analytics takes that insight to the next level by suggesting the optimal way to handle that future situation. Organizations that can act fast in dynamic conditions and make superior decisions in uncertain environments gain a strong competitive advantage.

With prescriptive analytics, you can:

  • Automate the complex decisions and trade-offs to better manage your limited resources.
  • Take advantage of a future opportunity or mitigate a future risk.
  • Proactively update recommendations based on changing events.
  • Meet operational goals, increase customer loyalty, prevent threats and fraud, and optimize business processes.

Use decision optimization

Step 1: Import the library

Run the following code to import Decision Optimization CPLEX Modeling library. The DOcplex library contains the two modeling packages, Mathematical Programming and Constraint Programming, referred to earlier.


In [ ]:
import sys
try:
    import docplex.mp
except:
    raise Exception('Please install docplex. See https://pypi.org/project/docplex/')

A restart of the kernel might be needed.

Step 2: Set up the prescriptive model

Writing a mathematical model

Convert the descriptive model into a mathematical model:

  • Use the two decision variables DeskProduction and CellProduction
  • Use the data given in the problem description (remember to convert minutes to hours where appropriate)
  • Write the objective as a mathematical expression
  • Write the constraints as mathematical expressions (use “=”, “<=”, or “>=”, and name the constraints to describe their purpose)
  • Define the domain for the decision variables

Telephone production: a mathematical model

To express the last two constraints, we model assembly time and painting time as linear combinations of the two productions, resulting in the following mathematical model:

maximize: 12 desk_production+20 cell_production

subject to:
desk_production>=100
cell_production>=100
0.2 desk_production+0.4 cell_production<=400
0.5 desk_production+0.4 cell_production<=490 </code>


In [ ]:
# first import the Model class from docplex.mp
from docplex.mp.model import Model

# create one model instance, with a name
m = Model(name='telephone_production')

The continuous variable desk represents the production of desk telephones. The continuous variable cell represents the production of cell phones.


In [ ]:
# by default, all variables in Docplex have a lower bound of 0 and infinite upper bound
desk = m.integer_var(name='desk')
cell = m.integer_var(name='cell')

In [ ]:
m.maximize(12 * desk + 20 * cell)

# write constraints
# constraint #1: desk production is greater than 100
m.add_constraint(desk >= 100, "desk")

# constraint #2: cell production is greater than 100
m.add_constraint(cell >= 100, "cell")

# constraint #3: assembly time limit
ct_assembly = m.add_constraint( 0.2 * desk + 0.4 * cell <= 400, "assembly_limit")

# constraint #4: paiting time limit
ct_painting = m.add_constraint( 0.5 * desk + 0.4 * cell <= 490, "painting_limit")

Solve with Decision Optimization

If you're using a Community Edition of CPLEX runtimes, depending on the size of the problem, the solve stage may fail and will need a paying subscription or product installation.

You will get the best solution found after n seconds, thanks to a time limit parameter.


In [ ]:
m.print_information()
msol = m.solve()

In [ ]:
assert msol is not None, "model can't solve"
m.print_solution()

Step 3: Modify the model

Modify constraints and variables bounds

The model object provides getters to retrieve variables and constraints by name:

  • get_var_by_name
  • get_constraint_by_name The variable and constraint objects both provide properties to access the right hand side (rhs) and left hand side (lhs). When you modify a rhs or lhs of a variable, you of course need to give a number. When you modify a rhs or lhs of a constraint, you can give a number or an expression based on variables.

Let's say we want to build 2000 cells and 1000 desks maximum.

And let's say we want to increase the production of both of them from 100 to 350


In [ ]:
# Access by name
m.get_var_by_name("desk").ub = 2000
# acess via the object
cell.ub = 1000


m.get_constraint_by_name("desk").rhs = 350
m.get_constraint_by_name("cell").rhs = 350

In [ ]:
msol = m.solve()
assert msol is not None, "model can't solve"
m.print_solution()

The production plan has been updated accordingly to our small changes.

Modify expressions

We now want to introduce a new type of product: the "hybrid" telephone.


In [ ]:
hybrid = m.integer_var(name='hybrid')

We need to:

  • introduce it in the objective
  • introduce it in the existing painting and assembly time constraints
  • add a new constraint for its production to produce at least 350 of them.

In [ ]:
m.add_constraint(hybrid >= 350)
;

The objective will move from maximize: 12 desk_production+20 cell_production to maximize: 12 desk_production+20 cell_production + 10 hybrid_prodction


In [ ]:
m.get_objective_expr().add_term(hybrid, 10)
;

The time constraints will be updated from 0.2 desk_production+0.4 cell_production<=400 0.5 desk_production+0.4 cell_production<=490 to 0.2 desk_production+0.4 cell_production + 0.2 hybrid_production<=400 0.5 desk_production+0.4 cell_production + 0.2 hybrid_production<=490

When you add a constraint to a model, its object is returned to you by the method add_constraint. If you don't have it, you can access it via its name


In [ ]:
m.get_constraint_by_name("assembly_limit").lhs.add_term(hybrid, 0.2)
ct_painting.lhs.add_term(hybrid, 0.2)
;

We can now compute the new production plan for our 3 products


In [ ]:
msol = m.solve()
assert msol is not None, "model can't solve"
m.print_solution()

Let's now say we improved our painting process, the distribution of the coefficients in the painting limits is not [0.5, 0.4, 0.2] anymore but [0.1, 0.1, 0.1] When you have the hand on an expression, you can modify the coefficient variable by variable with set_coefficient or via a list of (variable, coeff) with set_coefficients


In [ ]:
ct_painting.lhs.set_coefficients([(desk, 0.1), (cell, 0.1), (hybrid, 0.1)])

In [ ]:
msol = m.solve()
assert msol is not None, "model can't solve"
m.print_solution()

Relaxations

Let's now introduce a new constraint: polishing time limit.


In [ ]:
# constraint: polishing time limit
ct_polishing = m.add_constraint( 0.6 * desk + 0.6 * cell + 0.3 * hybrid <= 290, "polishing_limit")

In [ ]:
msol = m.solve()
if msol is None:
    print("model can't solve")

The model is now infeasible. We need to handle it and dig into the infeasibilities.

You can now use the Relaxer object. You can control the way it will relax the constraints or you can use 1 of the various automatic modes:

  • 'all' relaxes all constraints using a MEDIUM priority; this is the default.
  • 'named' relaxes all constraints with a user name but not the others.
  • 'match' looks for priority names within constraint names; unnamed constraints are not relaxed.

We will use the 'match' mode. Polishing constraint is mandatory. Painting constraint is a nice to have. Assembly constraint has low priority.


In [ ]:
ct_polishing.name = "high_"+ct_polishing.name
ct_assembly.name = "low_"+ct_assembly.name
ct_painting.name = "medium_"+ct_painting.name

In [ ]:
# if a name contains "low", it has priority LOW
# if a ct name contains "medium" it has priority MEDIUM
# same for HIGH
# if a constraint has no name or does not match any, it is not relaxable.
from docplex.mp.relaxer import Relaxer
relaxer = Relaxer(prioritizer='match', verbose=True)

relaxed_sol = relaxer.relax(m)
relaxed_ok = relaxed_sol is not None
assert relaxed_ok, "relaxation failed"
relaxer.print_information()

In [ ]:
m.print_solution()

In [ ]:
ct_polishing_relax = relaxer.get_relaxation(ct_polishing)
print("* found slack of {0} for polish ct".format(ct_polishing_relax))
ct_polishing.rhs+= ct_polishing_relax
m.solve()
m.report()
m.print_solution()

Summary

You learned how to set up and use the IBM Decision Optimization CPLEX Modeling for Python to formulate a Mathematical Programming model and modify it in various ways.

References

Copyright © 2017-2019 IBM. Sample Materials.


In [ ]: