Friday, April 6, 2018

Python constraint solver

In [1] a small problem is stated:

  • We need to assign 7 devices to 3 locations
  • The maximum number of devices assigned to a location is: loc1:3, loc2:4, loc3:2
  • Not all assignments are allowed. The allowed assignments are:
       loc1: device1, device3, device7,
       loc2: device1, device3, device4, device5, device6, device7
       loc3: device2, device4, device5, device6
  • Find all possible solutions
There is no objective and we need to enumerate all solutions. It makes sense to look at a Constraint Programming (CP) solver. A simple one is Python-constraints [2].

This looks like an assignment problem so we can define the decision variable: \[x_{L,d} = \begin{cases} 1 & \text{if device $d$ is assigned to location $L$} \\ 0 & \text{otherwise}\end{cases}\]

There are two main approaches to deal with the assignments that are not allowed.

  1. Generate a full matrix \(x_{L,d}\) and add constraints \(x_{L,d}=0\) if an assignment is not allowed.
  2. Generate only the variables \(x_{L,d}\) that are allowed.
I am partial to the second approach. With modern solvers there may not be much reason for this, but I have the feeling I know more what I am doing if I pay attention to such things. It forces me to be less sloppy.

The constraints are not very difficult:\[\begin{align} &\sum_{L|\mathit{allowed}(L,d)} x_{L,d} = 1 &\forall d\\ & \sum_{d|\mathit{allowed}(L,d)} x_{L,d} \le \mathit{MaxDev}_L& \forall L \end{align}\]

The Python code can look like:


from constraint import *

D = 7 # number of devices
L = 3 # number of locations

maxdev = [3,4,2]
allowed = [[1,3,7],[1,3,4,5,6,7],[2,4,5,6]]

problem = Problem()
problem.addVariables(["x_L%d_d%d" %(loc+1,d+1) for loc in range(L) for d in range(D) if d+1 in allowed[loc]],[0,1])
for loc in range(L):
    problem.addConstraint(MaxSumConstraint(maxdev[loc]),["x_L%d_d%d" %(loc+1,d+1) for d in range(D) if d+1 in allowed[loc]])
for d in range(D):
    problem.addConstraint(ExactSumConstraint(1),["x_L%d_d%d" %(loc+1,d+1) for loc in range(L) if d+1 in allowed[loc]])

S = problem.getSolutions()
n = len(S)
n

As you can see, I use the allowed list everywhere to restrict the tuples \((L,d)\). This solves very fast. There are 25 solutions for this small problem:




Notes:

  • If you want to solve this as a MIP use a dummy objective, and preferably use the solution pool technology available in solvers like Cplex and Gurobi. This is also very fast. The picture above was from a GAMS/Cplex run.
  • I expect the number of the solutions to quickly rise when the problem becomes bigger.
  • The Python code above assume short lists of allowed devices in each location. If these lists become long, it is better to use a different data structure such as dicts.
  • I don't think the Python code is very easy to read. I don't immediately recognize our mathematical constraints in the code.

References


  1. Python constraints - constraining the amount, https://stackoverflow.com/questions/49655875/python-constraints-constraining-the-amount
  2. Python-constraint, Constraint Solving Problem resolver for Python, https://labix.org/python-constraint