This tutorial includes everything you need to set up decision optimization engines, build a mathematical programming model, leveraging logical constraints.
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:
Logical constraints let you use the truth value of constraints inside the model. The truth value of a constraint is a binary variable equal to 1 when the constraint is satisfied, and equal to 0 when not. Adding a constraint to a model ensures that it is always satisfied. With logical constraints, one can use the truth value of a constraint inside the model, allowing to choose dynamically whether a constraint is to be satisfied (or not).
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:
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.
A discrete linear constraint is built from discrete coefficients and discrete variables, that is variables with type integer
or binary
.
For example, assuming x and y are integer variables:
2x+3y == 1
is discretex+y = 3.14
is not (because of 3.14)1.1 x + 2.2 y <= 3
is not because of the non-integer coefficients 1.1 and 2.2The truth value of a linear constraint is accessed by the status_var
property. This property returns a binary which can be used anywhere a variable can. However, the value of the truth value variable and the constraint are linked, both ways:
In the following small model, we show that the truth value of a constraint which has been added to a model is always equal to 1.
In [ ]:
from docplex.mp.model import Model
m1 = Model()
x = m1.integer_var(name='ix')
y = m1.integer_var(name='iy')
ct = m1.add(x + y <= 3)
# acces the truth value of a linear constraint
ct_truth = ct.status_var
m1.maximize(x+y)
assert m1.solve()
print('the truth value of [{0!s}] is {1}'.format(ct, ct_truth.solution_value))
A constraint that is not added to a model, has no effect. Its truth value is free: it can be either 1 or 0.
In the following example, both x
and y
are set to their upper bound, so that the constraint is not satisfied; hence the truth value is 0.
In [ ]:
m2 = Model(name='logical2')
x = m2.integer_var(name='ix', ub=4)
y = m2.integer_var(name='iy', ub=4)
ct = (x + y <= 3)
ct_truth = ct.status_var # not m2.add() here!
m2.maximize(x+y)
assert m2.solve()
m2.print_solution()
print('the truth value of [{0!s}] is {1}'.format(ct, ct_truth.solution_value))
We have learned about the truth value variable of linear constraints, but there's more. Linear constraints can be freely used in expressions: Docplex will then substitute the constraint's truth value variable in the expression.
Let's experiment again with a toy model: in this model,
we want to express that when x ==3
is false, then y ==4
must also be false.
To express this, it suffices to say that the truth value of y == 4
is less than or equal
to the truth value of x ==3
. When x==3
is false, is truthe value is 0, hence the truth value of y==4
is also zero, and y
cannot be equal to 4.
However, as shown in the model below, it is not necessary to use the status_var
propert: using
the constraints in a comparison expression works fine.
As we maximize y, y has value 4 in the optimal solution (it is the upper bound), and consequently the constraint ct_y4
is satisfied. From the inequality between truth values,
it follows that the truth value of ct_x2
equals 1 and x is equal to 2.
Using the constraints in the inequality has silently converted each constraint into its truth value.
In [ ]:
m3 = Model(name='logical3')
x = m3.integer_var(name='ix', ub=4)
y = m3.integer_var(name='iy', ub=4)
ct_x2 = (x == 2)
ct_y4 = (y == 4)
# use constraints in comparison
m3.add( ct_y4 <= ct_x2 )
m3.maximize(y)
assert m3.solve()
# expected solution x==2, and y==4.
m3.print_solution()
Constraint truth values can be used with arithmetic operators, just as variables can. In the next model, we express a (slightly) more complex constraint:
Let's see how we can express this easily with truth values:
In [ ]:
m31 = Model(name='logical31')
x = m31.integer_var(name='ix', ub=4)
y = m31.integer_var(name='iy', ub=10)
z = m31.integer_var(name='iz', ub=10)
ct_x2 = (x == 3)
ct_y5 = (y == 5)
ct_z5 = (z == 5)
#either ct_x2 is true or -both- ct_y5 and ct_z5 must be true
m31.add( 2 * ct_x2 + (ct_y5 + ct_z5) == 2)
# force x to be less than 2: it cannot be equal to 3!
m31.add(x <= 2)
# maximize sum of x,y,z
m31.maximize(x+y+z)
assert m31.solve()
# the expected solution is: x=2, y=5, z=5
assert m31.objective_value == 12
m31.print_solution()
As we have seen, constraints can be used in expressions. This includes the Model.sum()
and Model.dot()
aggregation methods.
In the next model, we define ten variables, one of which must be equal to 3 (we dpn't care which one, for now). As we maximize the sum of all xs
variables, all will end up equal to their upper bound, except for one.
In [ ]:
m4 = Model(name='logical4')
xs = m4.integer_var_list(10, ub=100)
cts = [xi==3 for xi in xs]
m4.add( m4.sum(cts) == 1)
m4.maximize(m4.sum(xs))
assert m4.solve()
m4.print_solution()
As we can see, all variables but one are set to their upper bound of 100. We cannot predict which variable will be set to 3. However, let's imagine that we prefer variable with a lower index to be set to 3, how can we express this preference?
The answer is to use an additional expression to the objective, using a scalar product of constraint truth value
In [ ]:
preference = m4.dot(cts, (k+1 for k in range(len(xs))))
# we prefer lower indices for satisfying the x==3 constraint
# so the final objective is a maximize of sum of xs -minus- the preference
m4.maximize(m4.sum(xs) - preference)
assert m4.solve()
m4.print_solution()
As expected, the x
variable set to 3 now is the first one.
Truth values can be used to negate a complex constraint, by forcing its truth value to be equal to 0.
In the next model, we illustrate how an equality constraint can be negated by forcing its truth value to zero. This negation forbids y to be equal to 4, as it would be without this negation. Finally, the objective is 7 instead of 8.
In [ ]:
m5 = Model(name='logical5')
x = m5.integer_var(name='ix', ub=4)
y = m5.integer_var(name='iy', ub=4)
# this is the equality constraint we want to negate
ct_xy7 = (y + x >= 7)
# forcing truth value to zero means the constraint is not satisfied.
# note how we use a constraint in an expression
negation = m5.add( ct_xy7 == 0)
# maximize x+y should yield both variables to 4, but x+y cannot be greater than 7
m5.maximize(x + y)
assert m5.solve()
m5.print_solution()
# expecting 6 as objective, not 8
assert m5.objective_value == 6
# now remove the negation
m5.remove_constraint(negation)
# and solve again
assert m5.solve()
# the objective is 8 as expected: both x and y are equal to 4
assert m5.objective_value == 8
m5.print_solution()
We have seen that linear constraints have an associated binary variable, its truth value, whose value is linked to whether or not the constraint is satisfied.
second, linear constraints can be freely mixed with variables in expression to express meta-constraints that is, constraints about constraints. As an example, we have shown how to use truth values to negate constraints.
In [ ]:
m6 = Model(name='logical6')
x = m6.integer_var(name='ix', ub=4)
y = m6.integer_var(name='iy', ub=4)
# this is the equality constraint we want to negate
m6.add(x +1 <= y)
m6.add(x != 3)
m6.add(y != 4)
# forcing truth value to zero means the constraint is not satisfied.
# note how we use a constraint in an expression
m6.add(x+y <= 7)
# maximize x+y should yield both variables to 4,
# but here: x < y, y cannot be 4 thus x cannot be 3 either so we get x=2, y=3
m6.maximize(x + y)
assert m6.solve()
m6.print_solution()
# expecting 5 as objective, not 8
assert m6.objective_value == 5
As we have seen, using a constraint in expressions automtically generates a truth value variable, whose value is linked to the status of the constraint.
However, in some cases, it can be useful to relate the status of a constraint to an existing binary variable. This is the purpose of equivalence constraints.
An equivalence constraint relates an existing binary variable to the status of a discrete linear constraints, in both directions. The syntax is:
`Model.add_equivalence(bvar, linear_ct, active_value, name)`
bvar
is the existing binary variablelinear-ct
is a discrete linear constraintactive_value
can take values 1 or 0 (the default is 1)name
is an optional string to name the equivalence.If the binary variable bvar
equals 1, then the constraint is satisfied. Conversely, if the constraint is satisfied, the binary variable is set to 1.
In [ ]:
m7 = Model(name='logical7')
size = 7
il = m7.integer_var_list(size, name='i', ub=10)
jl = m7.integer_var_list(size, name='j', ub=10)
bl = m7.binary_var_list(size, name='b')
for k in range(size):
# for each i, relate bl_k to il_k==5 *and* jl_k == 7
m7.add_equivalence(bl[k], il[k] == 5)
m7.add_equivalence(bl[k], jl[k] == 7)
# now maximize sum of bs
m7.maximize(m7.sum(bl))
assert m7.solve()
m7.print_solution()
The equivalence constraint decsribed in the previous section links the value of an existing binary variable to the satisfaction of a linear constraint. In certain cases, it is sufficient to link from an existing binary variable to the constraint, but not the other way. This is what indicator constraints do.
The syntax is very similar to equivalence:
`Model.add_indicator(bvar, linear_ct, active_value=1, name=None)`
bvar
is the existing binary variablelinear-ct
is a discrete linear constraintactive_value
can take values 1 or 0 (the default is 1)name
is an optional string to name the indicator.
The indicator constraint works as follows: if the binary variable is set to 1, the constraint is satified; if the binary variable is set to 0, anything can happen.
One noteworty difference between indicators and equivalences is that, for indicators, the linear constraint need not be discrete.
In the following small model, we first solve without the indicator: both b and x are set to their upper bound, and the final objective is 200.
Then we add an indicator sttaing that when b equals1, then x must be less than 3.14; the resulting objective is 103.14, as b is set to 1, which trigger the x <= 31.4
constraint.
Note that the right-hand side constraint is not discrete (because of 3.14).
In [ ]:
m8 = Model(name='logical8')
x = m8.continuous_var(name='x', ub=100)
b = m8.binary_var(name='b')
m8.maximize(100*b +x)
assert m8.solve()
assert m8.objective_value == 200
m8.print_solution()
ind_pi = m8.add_indicator(b, x <= 3.14)
assert m8.solve()
assert m8.objective_value <= 104
m8.print_solution()
In this section we explore the Model.add_if_then
construct which links the truth value of two constraints:
Model.add_if_then(if_ct, then_ct)
ensures that, when constraint if_ct
is satisfied, then then_ct
is also satisfied.
When if_ct
is not satisfied, then_ct
is free to be satsfied or not.
The syntax is:
`Model.add_if_then(if_ct, then_ct, negate=False)`
if_ct
is a discrete linear constraintthen_ct
is any linear constraint (not necessarily discrete),negate
is an optional flag to reverse the logic, that is satisfy then_ct
if if_ct
is not (more on this later)
As for indicators, the then_ct
need not be discrete.
Model.add_if_then(if_ct, then_ct)
is roughly equivalent to Model.add_indicator(if_ct.status_var, then_ct)
.
In [ ]:
m9 = Model(name='logical9')
x = m9.continuous_var(name='x', ub=100)
y = m9.integer_var(name='iy', ub = 11)
z = m9.integer_var(name='iz', ub = 13)
m9.add_if_then(y+z >= 10, x <= 3.14)
# y and z are puashed to their ub, so x is down to 3.14
m9.maximize(x + 100*(y + z))
m9.solve()
m9.print_solution()
In this second variant, the objective coefficient for (y+z)
is 2 instead of 100, so x
domines the objective, and reache sits upper bound, while (y+z) must be less than 9, which is what we observe.
In [ ]:
# y and z are pushed to their ub, so x is down to 3.14
m9.maximize(x + 2 *(y + z))
m9.solve()
m9.print_solution()
assert abs(m9.objective_value - 118) <= 1e-2
We have seen that linear constraints have an associated binary variable, its truth value, whose value is linked to whether or not the constraint is satisfied.
second, linear constraints can be freely mixed with variables in expression to express meta-constraints that is, constraints about constraints. As an example, we have shown how to use truth values to negate constraints.
In addition, we have learned to use equivalence, indicator and if_then constraints.
You learned how to set up and use the IBM Decision Optimization CPLEX Modeling for Python to formulate a Mathematical Programming model with logical constraints.
Copyright © 2017-2019 IBM. Sample Materials.
In [ ]: