Friday, June 12, 2020

Designing a sports schedule



In [1] a question was posed about designing a sports schedule.

We have the following information:

  • There are 24 teams
  • There are 23 rounds
  • In every round, each team plays against another team. A team plays exactly once in a round. I.e., there are 12 games per round.
  • A team \(t1\) plays against team \(t2\) exactly once.

This is actually not too difficult to model.

Math Programming Model


We define as binary decision variable: \[\mathit{game}_{t1,t2,r} = \begin{cases} 1 & \text{if team \(t1\) plays against team \(t2\) in round \(r\)}\\ 0 & \text{otherwise} \end{cases}\] To prevent double counting, we use the convention that \(\mathit{game}_{t1,t2,r}\) is only defined when \(t1 \lt t2\). So, we have in the model \(\mathit{game}_{team2,team8,round4}\), but we will never use: \(\mathit{game}_{team8,team2,round4}\). This is a little bit of a complicating factor in the model, but the pay-off is that the bookkeeping becomes in the end simpler.

With this we can formulate:

Mixed-Integer Programming Model
\[\begin{align}\min\>& 0 && &&\text{dummy objective} \\ & \sum_{t2|t1\lt t2} \color{darkred}{\mathit{game}}_{t1,t2,r} + \sum_{t2|t2\lt t1} \color{darkred}{\mathit{game}}_{t2,t1,r} = 1 && \forall t1,r &&\text{play exactly one opponent each round}\\ & \sum_r \color{darkred}{\mathit{game}}_{t1,t2,r}=1&&\forall t1,t2|t1 \lt t2 && \text{meet opponent exactly once}\\ &\color{darkred}{\mathit{game}}_{t1,t2,r} \in \{0,1\} &&\forall t1,t2,r|t1 \lt t2 &&\text{binary variables} \end{align}\]

The model does not have a real objective: we are just looking for a feasible solution. The first constraint is the most complicated one. Basically it says \[\sum_{t2} {\mathit{game}}_{t1,t2,r} = 1 \>\> \forall t1,r\] but as we only use \({\mathit{game}}_{t1,t2,r}\) for \(t1 \lt t2\), we need to split things in two. This can be visualized as follows:

Opponents of team t3

If we would not have excluded all \(t2\ge t1\), but used a fully allocated \({\mathit{game}}_{t1,t2,r}\) we would have needed to add the constraints: \[\begin{align} & {\mathit{game}}_{t1,t2,r} = {\mathit{game}}_{t2,t1,r} \\ & {\mathit{game}}_{t1,t1,r}=0 \end{align}\] This will make the model larger, but a good presolver should help with that. Although for this model it probably does not make much difference, I prefer explicitly excluding all \(t2\ge t1\).

This is a rather simple model. But it will deliver the schedules we are looking for:



----     24 VARIABLE game.L  schedule for teams

                   round1      round2      round3      round4      round5      round6      round7      round8

team1 .team15                                   1
team1 .team16                                               1
team1 .team17                                                                                   1
team1 .team18                                                           1
team1 .team19           1
team1 .team20                                                                                               1
team1 .team22                                                                       1
team1 .team24                       1
team2 .team12                                                                                   1
team2 .team15                                               1
team2 .team16                                                                                               1
team2 .team19                       1
team2 .team20                                                           1
team2 .team21                                                                       1
team2 .team23           1
team2 .team24                                   1
team3 .team12           1
team3 .team13                                                                       1
team3 .team14                                                                                               1
team3 .team15                       1
team3 .team16                                                                                   1
team3 .team21                                                           1
team3 .team23                                   1
team3 .team24                                               1
team4 .team10                                                                                               1
team4 .team14                                                                                   1
team4 .team15           1
team4 .team16                                                           1
team4 .team18                       1
team4 .team21                                               1
team4 .team22                                   1
team4 .team23                                                                       1
team5 .team11                                               1
team5 .team15                                                           1
team5 .team16                       1
team5 .team18                                                                                   1
team5 .team19                                                                                               1
team5 .team20                                                                       1
team5 .team21                                   1
team5 .team22           1
team6 .team9                                    1
team6 .team10                                                           1
team6 .team11           1
team6 .team16                                                                       1
team6 .team17                                                                                               1
team6 .team20                                                                                   1
team6 .team22                                               1
team6 .team23                       1
team7 .team9                        1
team7 .team10                                                                                   1
team7 .team12                                                           1
team7 .team14                                                                       1
team7 .team16                                   1
team7 .team19                                               1
team7 .team21                                                                                               1
team7 .team24           1
team8 .team9                                                1
team8 .team11                                   1
team8 .team12                                                                       1
team8 .team13                                                                                               1
team8 .team14                       1
team8 .team16           1
team8 .team19                                                           1
team8 .team22                                                                                   1
team9 .team18                                                                       1
team9 .team20           1
team9 .team21                                                                                   1
team9 .team22                                                           1
team9 .team23                                                                                               1
team10.team17           1
team10.team20                                   1
team10.team21                       1
team10.team23                                               1
team10.team24                                                                       1
team11.team17                       1
team11.team18                                                                                               1
team11.team19                                                                       1
team11.team23                                                                                   1
team11.team24                                                           1
team12.team17                                   1
team12.team18                                               1
team12.team22                       1
team12.team24                                                                                               1
team13.team17                                               1
team13.team18           1
team13.team19                                   1
team13.team20                       1
team13.team23                                                           1
team13.team24                                                                                   1
team14.team17                                                           1
team14.team18                                   1
team14.team20                                               1
team14.team21           1
team15.team17                                                                       1
team15.team19                                                                                   1
team15.team22                                                                                               1

            +      round9     round10     round11     round12     round13     round14     round15     round16

team1 .team9                                                                                                1
team1 .team10                                                                                   1
team1 .team11                                                                       1
team1 .team12                                                           1
team1 .team13                                               1
team1 .team14                                   1
team1 .team21                       1
team1 .team23           1
team2 .team9                                                                                    1
team2 .team10                                               1
team2 .team11                                                           1
team2 .team13                       1
team2 .team14           1
team2 .team17                                                                                               1
team2 .team18                                                                       1
team2 .team22                                   1
team3 .team9                                                                        1
team3 .team10                                   1
team3 .team11                                                                                               1
team3 .team17                                                           1
team3 .team18                                                                                   1
team3 .team19                       1
team3 .team20           1
team3 .team22                                               1
team4 .team9                                                            1
team4 .team11           1
team4 .team12                       1
team4 .team13                                                                       1
team4 .team17                                                                                   1
team4 .team19                                                                                               1
team4 .team20                                   1
team4 .team24                                               1
team5 .team9                                                1
team5 .team10                                                           1
team5 .team12                                   1
team5 .team13                                                                                               1
team5 .team14                                                                                   1
team5 .team17                                                                       1
team5 .team23                       1
team5 .team24           1
team6 .team12           1
team6 .team13                                                                                   1
team6 .team14                       1
team6 .team15                                                                       1
team6 .team18                                               1
team6 .team19                                                           1
team6 .team21                                                                                               1
team6 .team24                                   1
team7 .team11                                               1
team7 .team13                                   1
team7 .team15                                                                                               1
team7 .team17                       1
team7 .team18           1
team7 .team20                                                           1
team7 .team22                                                                                   1
team7 .team23                                                                       1
team8 .team10                       1
team8 .team15           1
team8 .team17                                               1
team8 .team18                                   1
team8 .team20                                                                       1
team8 .team21                                                                                   1
team8 .team23                                                                                               1
team8 .team24                                                           1
team9 .team17                                   1
team9 .team19           1
team9 .team24                       1
team10.team18                                                                                               1
team10.team19                                                                       1
team10.team22           1
team11.team20                                                                                   1
team11.team21                                   1
team11.team22                       1
team12.team19                                                                                   1
team12.team20                                                                                               1
team12.team21                                                                       1
team12.team23                                               1
team13.team21           1
team13.team22                                                           1
team14.team19                                               1
team14.team22                                                                                               1
team14.team23                                                           1
team14.team24                                                                       1
team15.team18                                                           1
team15.team20                       1
team15.team21                                               1
team15.team23                                   1
team15.team24                                                                                   1
team16.team17           1
team16.team18                       1
team16.team19                                   1
team16.team20                                               1
team16.team21                                                           1
team16.team22                                                                       1
team16.team23                                                                                   1
team16.team24                                                                                               1

            +     round17     round18     round19     round20     round21     round22     round23

team1 .team2                                                                                    1
team1 .team3                                                                        1
team1 .team4                                                            1
team1 .team5                                                1
team1 .team6                                    1
team1 .team7                        1
team1 .team8            1
team2 .team3                                                            1
team2 .team4                                                                        1
team2 .team5                                    1
team2 .team6                                                1
team2 .team7            1
team2 .team8                        1
team3 .team4                                                                                    1
team3 .team5                        1
team3 .team6            1
team3 .team7                                                1
team3 .team8                                    1
team4 .team5            1
team4 .team6                        1
team4 .team7                                    1
team4 .team8                                                1
team5 .team6                                                                                    1
team5 .team7                                                                        1
team5 .team8                                                            1
team6 .team7                                                            1
team6 .team8                                                                        1
team7 .team8                                                                                    1
team9 .team10                                                                                   1
team9 .team11                                                                       1
team9 .team12                                                           1
team9 .team13                                               1
team9 .team14                                   1
team9 .team15                       1
team9 .team16           1
team10.team11                                                           1
team10.team12                                                                       1
team10.team13                                   1
team10.team14                                               1
team10.team15           1
team10.team16                       1
team11.team12                                                                                   1
team11.team13                       1
team11.team14           1
team11.team15                                               1
team11.team16                                   1
team12.team13           1
team12.team14                       1
team12.team15                                   1
team12.team16                                               1
team13.team14                                                                                   1
team13.team15                                                                       1
team13.team16                                                           1
team14.team15                                                           1
team14.team16                                                                       1
team15.team16                                                                                   1
team17.team18                                                                                   1
team17.team19                                                                       1
team17.team20                                                           1
team17.team21                                               1
team17.team22                                   1
team17.team23                       1
team17.team24           1
team18.team19                                                           1
team18.team20                                                                       1
team18.team21                                   1
team18.team22                                               1
team18.team23           1
team18.team24                       1
team19.team20                                                                                   1
team19.team21                       1
team19.team22           1
team19.team23                                               1
team19.team24                                   1
team20.team21           1
team20.team22                       1
team20.team23                                   1
team20.team24                                               1
team21.team22                                                                                   1
team21.team23                                                                       1
team21.team24                                                           1
team22.team23                                                           1
team22.team24                                                                       1
team23.team24                                                                                   1


This model solved very quickly, it needed about 0.2 seconds. Of course, the solution is not unique.


GAMS representation


set
  t
'team' /team1*team24/
  r
'rounds' /round1*round23/
;

alias (t,t1,t2);

set lt(t1,t2) 'less than';
lt(t1,t2) =
ord(t1)<ord(t2);

binary variable game(t1,t2,r) 'schedule for teams';
variable dummy 'objective variable';

equations
   round(r,t1)
'play exactly one opponent each round'
   meet(t1,t2)
't1 meets t2 exactly once'
   obj        
'dummy objective'
;

round(r,t1)..
  
sum(lt(t1,t2),game(t1,t2,r))+sum(lt(t2,t1),game(t2,t1,r)) =e= 1;
meet(lt(t1,t2))..
sum(r,game(t1,t2,r)) =e= 1;
obj.. dummy=e=0;

model m /all/;
solve m minimizing dummy using mip;

option game:0;
display game.l;



Notice how the set lt(t1,t2) implements the strictly upper-triangular structure we want. We use the GAMS feature that if a variable does not appear in any of the equations, it will not be generated, even though we declared all game(t1,t2,r). Note that we don't need to set a gap using the optcr option. The solver will stop as soon as it has found a feasible integer solution: that solution is immediately optimal.

References


  1. Excel solver failure to get optimal solution when generating a schedule for 12 week schedule for 24 teams, https://stackoverflow.com/questions/62327762/excel-solver-failure-to-get-optimal-solution-when-generating-a-schedule-for-12-w

A Set Partitioning Problem and Infeasible Data Sets

In [1], a problem is stated that can be interpreted as a Set Partitioning Problem [2].

Consider the following data:


----     16 PARAMETER data  input data

            start      length        cost

job1            2           5          47
job2           19           5          20
job3           11           2          38
job4            5           2          14
job5            5           8          40
job6            3          10          26
job7            6           3          68
job8           19           5          61
job9                       10          80
job10          10           4          37
job11          22           2          70
job12          12           7          78
job13          22           2          67
job14          17           7          35
job15           1           4          17
job16          13           4          19
job17           1           8          68
job18           4           9          59
job19          14           8          12
job20           8           6          82

Note: zeros are not printed.

The problem is:

Find a subset of jobs that don't overlap and cover each hour 0 through 23, and that minimize cost.

 This is slightly different than the data in [1], in order to make the problem feasible.

Set Partitioning Model  


Before we can work on the model itself, we need to develop some derived data. We introduce the boolean parameter \(\mathit{Cover}_{i,t}\) defined by: \[\mathit{Cover}_{i,t}=\begin{cases} 1 & \text{if job $i$ covers hour $t$}\\ 0 & \text{otherwise}\end{cases}\] This parameter looks like:


----     16 PARAMETER cover  coverage of hours by jobs

              h00         h01         h02         h03         h04         h05         h06         h07         h08

job1                                    1           1           1           1           1
job4                                                                        1           1
job5                                                                        1           1           1           1
job6                                                1           1           1           1           1           1
job7                                                                                    1           1           1
job9            1           1           1           1           1           1           1           1           1
job15                       1           1           1           1
job17                       1           1           1           1           1           1           1           1
job18                                                           1           1           1           1           1
job20                                                                                                           1

    +         h09         h10         h11         h12         h13         h14         h15         h16         h17

job3                                    1           1
job5            1           1           1           1
job6            1           1           1           1
job9            1
job10                       1           1           1           1
job12                                               1           1           1           1           1           1
job14                                                                                                           1
job16                                                           1           1           1           1
job18           1           1           1           1
job19                                                                       1           1           1           1
job20           1           1           1           1           1

    +         h18         h19         h20         h21         h22         h23

job2                        1           1           1           1           1
job8                        1           1           1           1           1
job11                                                           1           1
job12           1
job13                                                           1           1
job14           1           1           1           1           1           1
job19           1           1           1           1


We further introduce the binary decision variables: \[x_i = \begin{cases} 1 & \text{if job $i$ is selected} \\ 0 & \text{otherwise}\end{cases}\] With this we can formulate:

Set Partitioning Model
\[\begin{align}\min & \sum_i \color{darkblue}{\mathit{Cost}}_i \cdot \color{darkred}x_i \\ & \sum_i \color{darkblue}{\mathit{Cover}}_{i,t} \cdot \color{darkred}x_i = 1 &&\forall t \\ & \color{darkred}x_i \in \{0,1\} \end{align}\]

When we solve this problem using the above data set we get the solution:


----     31 VARIABLE x.L  selected jobs

job9  1,    job10 1,    job13 1,    job19 1


----     31 VARIABLE tcost.L               =          196  total cost


It is noted that the model can also be stated using a set representation of the coverage data. Such a model can look like:


Set Partitioning Model 2
\[\begin{align}\min & \sum_i \color{darkblue}{\mathit{Cost}}_i \cdot \color{darkred}x_i \\ & \sum_{i|\color{darkblue}{\mathit{Cover}}(i,t)} \color{darkred}x_i = 1 &&\forall t \\ & \color{darkred}x_i \in \{0,1\} \end{align}\]

Although this looks somewhat different, it is really exactly the same. This is actually the form I use most of the time.

Infeasibilities


It is quite easy to see data sets that cause the problem to be infeasible. Indeed, the data set shown in [1] is actually yielding a model that is not feasible.

There are two ways in which the model can be infeasible. The first is the easy one: we have no jobs at all that cover a time period \(t\).  Consider the data:


----     16 PARAMETER data  input data

            start      length        cost

job1            2          12          63
job2           19           5          85
job3           11           2          31
job4            5           8          70
job5            5           2          80
job6            3           4          37
job7            6           9          20
job8           19           5          55
job9                        5          24
job10          10           5          89
job11          22           2          34
job12          12           2          36


----     16 PARAMETER cover  coverage of hours by jobs

              h00         h01         h02         h03         h04         h05         h06         h07         h08

job1                                    1           1           1           1           1           1           1
job4                                                                        1           1           1           1
job5                                                                        1           1
job6                                                1           1           1           1
job7                                                                                    1           1           1
job9            1           1           1           1           1

    +         h09         h10         h11         h12         h13         h14         h19         h20         h21

job1            1           1           1           1           1
job2                                                                                    1           1           1
job3                                    1           1
job4            1           1           1           1
job7            1           1           1           1           1           1
job8                                                                                    1           1           1
job10                       1           1           1           1           1
job12                                               1           1

    +         h22         h23

job2            1           1
job8            1           1
job11           1           1


We see that hours 15, 16, 17, and 18 are not covered by any job. This can be easily checked in advance. Even GAMS is complaining when generating the model:


**** Exec Error at line 24: Equation infeasible due to rhs value

**** INFEASIBLE EQUATIONS ...

---- ecover  =E=  

ecover(h15)..  0 =E= 1 ; (LHS = 0, INFES = 1 ****)
     
ecover(h16)..  0 =E= 1 ; (LHS = 0, INFES = 1 ****)
     
ecover(h17)..  0 =E= 1 ; (LHS = 0, INFES = 1 ****)
     
ecover(h18)..  0 =E= 1 ; (LHS = 0, INFES = 1 ****)


There is another form of infeasibility that is more difficult to diagnose. When we use the data set:


----     16 PARAMETER data  input data

            start      length        cost

job1            2           6          67
job2           19           5          52
job3           11           5          47
job4            5           2          20
job5            5           2          38
job6            3           8          14
job7            6          10          40
job8           19           3          26
job9                        8          68
job10          10          10          61
job11          22           2          80
job12          12           2          37
job13          22           2          70
job14          17           2          78
job15           1          11          67
job16          13           4          35
job17           1           4          17
job18           4           8          19
job19          14           9          68

we get:


               S O L V E      S U M M A R Y

     MODEL   m                   OBJECTIVE  tcost
     TYPE    MIP                 DIRECTION  MINIMIZE
     SOLVER  CPLEX               FROM LINE  28

**** SOLVER STATUS     1 Normal Completion         
**** MODEL STATUS      10 Integer Infeasible       
**** OBJECTIVE VALUE               NA


There is not much in diagnostics for most solvers. Cplex at least reports:


Infeasibility row 'ecover(h08)':  0  = 1.

This is better than most solvers, but may be difficult to digest for an end-user. In applications, it may be better to make sure the problem is never infeasible. Basically, allow the constraint to become infeasible but at a price. This is sometimes called an elastic formulation. There are two ways to do this for this model:

  1. use a data-driven method: introduce expensive emergency jobs used to make the constraint feasible
  2. change the constraint directly

Data-driven Elastic model 


We can add expensive filler-jobs to our data set as follows:


----     21 PARAMETER data  input data

               start      length        cost

job1               2           6          67
job2              19           5          52
job3              11           5          47
job4               5           2          20
job5               5           2          38
job6               3           8          14
job7               6          10          40
job8              19           3          26
job9                           8          68
job10             10          10          61
job11             22           2          80
job12             12           2          37
job13             22           2          70
job14             17           2          78
job15              1          11          67
job16             13           4          35
job17              1           4          17
job18              4           8          19
job19             14           9          68
notcov00                       1         999
notcov01           1           1         999
notcov02           2           1         999
notcov03           3           1         999
notcov04           4           1         999
notcov05           5           1         999
notcov06           6           1         999
notcov07           7           1         999
notcov08           8           1         999
notcov09           9           1         999
notcov10          10           1         999
notcov11          11           1         999
notcov12          12           1         999
notcov13          13           1         999
notcov14          14           1         999
notcov15          15           1         999
notcov16          16           1         999
notcov17          17           1         999
notcov18          18           1         999
notcov19          19           1         999
notcov20          20           1         999
notcov21          21           1         999
notcov22          22           1         999
notcov23          23           1         999


When we solve this, we see the following solution:


----     36 VARIABLE x.L  selected jobs

job12    1,    job15    1,    job19    1,    notcov00 1,    notcov23 1


----     36 VARIABLE tcost.L               =         2170  total cost


So we have trouble covering 2 hours with jobs. If we allow then to remain like that, the best solution is to use jobs 12, 15 and 19 for the other hours.


Elastic model: change constraint


Instead of changing the data, we can also do the same by changing the covering constraint:


Set Partitioning Model, elastic version
\[\begin{align}\min & \sum_i \color{darkblue}{\mathit{Cost}}_i \cdot \color{darkred}x_i + \color{darkblue}{\mathit{Penalty}} \cdot \sum_t\color{darkred}{\mathit{Uncov}}_t \\ & \sum_i \color{darkblue}{\mathit{Cover}}_{i,t} \cdot \color{darkred}x_i + \color{darkred}{\mathit{Uncov}}_t = 1 &&\forall t \\ & \color{darkred}x_i \in \{0,1\} \\ & \color{darkred}{\mathit{Uncov}}_t \in [0,1] \end{align}\]

You can see we added a slack variable \({\mathit{Uncov}}_t\), and included this in the objective with a penalty (999). When we run this model, we see:


----     44 VARIABLE x.L  selected jobs

job12 1,    job15 1,    job19 1


----     44 VARIABLE uncov.L  uncovered hours

h00 1,    h23 1


----     44 VARIABLE tcost.L               =         2170  total cost (objective)
            PARAMETER jobcost              =          172  cost related to running jobs
            PARAMETER penalties            =         1998  for uncovered hours


I use this type of elastic formulations a lot. This approach prevents a user from seeing "sorry model was infeasible". Instead, it gives back a meaningful result.

References


  1. Profit maximising job scheduling using Python, https://stackoverflow.com/questions/62297792/profit-maximising-job-scheduling-using-python
  2. Garfinkel, R. S., and G. L. Nemhauser. “The Set-Partitioning Problem: Set Covering with Equality Constraints.” Operations Research, vol. 17, no. 5, 1969, pp. 848–856.

Sunday, June 7, 2020

A facility location problem: debunking a myth

In [1] the following problem is proposed:

Consider a grid with 40,000 cells. We have customers located on this grid (this is data). We want to place stores on the grid such that each customer has at least one store within reach. Finally, we want to minimize the number of stores we need to use.

Furthermore:

so in a situation like mine I could easily end up with a linear system with millions of rows and columns, which with integer/binary variables will take about the age of the universe to solve.

Well, let's see...


Data


The first thing we do is generating some data. We introduce a grid of  \(200 \times 200\) with \(40,000\) cells. Then we randomly place \(M=500\) customers on this grid.



----     22 PARAMETER cloc  customer locations

                  x           y

cust1            35          75
cust2           169          84
cust3           111          18
cust4            61         163
cust5            59         102
cust6            45         147
cust7            70         165
cust8           172          83
cust9            14         185
cust10          101          79
. . .
cust495          83         186
cust496         174         159
cust497         196         148
cust498         115         136
cust499          63         101
cust500          92          87


Reach


Secondly, we create a large, sparse boolean parameter \(\mathit{reach}_{c,i,j}\) indicating if cell \((i,j)\) is within reach of customer \(c\). I used the rule: if the Manhattan distance between \(c\) and \((i,j)\) is 10 or less, we are within reach: \[ |i-\mathit{cloc}_{c,x}|+|j-\mathit{cloc}_{c,y}| \le 10\] This generates a large parameter. A fragment can look like:



----     29 PARAMETER reach  location (i,j) is within reach of customer

                      j1          j2          j3          j4          j5          j6          j7          j8          j9

cust3  .i110                                                                                                           1
cust3  .i111                                                                                               1           1
cust3  .i112                                                                                                           1
cust39 .i116                                                                                   1
cust39 .i117                                                                       1           1           1
cust39 .i118                                                           1           1           1           1           1
cust39 .i119                                               1           1           1           1           1           1
cust39 .i120                                   1           1           1           1           1           1           1
cust39 .i121                       1           1           1           1           1           1           1           1

There are 106,376 elements in this data structure.


MIP model


With this, we can formulate the set covering model:


Set covering model
\[\begin{align}\min\>& \color{darkred}{\mathit{numStores}} \\ & \color{darkred}{\mathit{numStores}} = \sum_{i,j} \color{darkred}{\mathit{placeStore}}_{i,j}\\ & \sum_{i,j} \color{darkblue}{\mathit{reach}}_{c,i,j} \cdot \color{darkred}{\mathit{placeStore}}_{i,j} \ge 1 & \forall c \\ &\color{darkred}{\mathit{placeStore}}_{i,j} \in \{0,1\} \end{align}\]


This is a large, but easy MIP model. The size is large:


MODEL STATISTICS

BLOCKS OF EQUATIONS           2     SINGLE EQUATIONS          501
BLOCKS OF VARIABLES           2     SINGLE VARIABLES       40,001
NON ZERO ELEMENTS       146,377     DISCRETE VARIABLES     40,000

But it solves very fast:

Found incumbent of value 495.000000 after 0.00 sec. (1.89 ticks)
Tried aggregator 8 times.
MIP Presolve eliminated 306 rows and 39764 columns.
MIP Presolve modified 6 coefficients.
Aggregator did 29 substitutions.
Reduced MIP has 166 rows, 208 columns, and 658 nonzeros.
Reduced MIP has 208 binaries, 0 generals, 0 SOSs, and 0 indicators.
Presolve time = 0.36 sec. (72.59 ticks)
Probing time = 0.00 sec. (0.05 ticks)
Tried aggregator 1 time.
Detecting symmetries...
MIP Presolve eliminated 1 rows and 5 columns.
Reduced MIP has 165 rows, 203 columns, and 644 nonzeros.
Reduced MIP has 203 binaries, 0 generals, 0 SOSs, and 0 indicators.
Presolve time = 0.00 sec. (0.42 ticks)
Probing time = 0.00 sec. (0.05 ticks)
Clique table members: 35.
MIP emphasis: balance optimality and feasibility.
MIP search method: dynamic search.
Parallel mode: deterministic, using up to 8 threads.
Parallel mode: deterministic, using up to 2 threads for parallel tasks at root LP.
Tried aggregator 1 time.
No LP presolve or aggregator reductions.
Presolve time = 0.00 sec. (0.09 ticks)

Iteration log . . .
Iteration:     1   Dual objective     =            57.000000
Iteration:    62   Dual objective     =            93.000000
Iteration:   124   Dual objective     =           109.000000
Iteration:   186   Dual objective     =           111.500000
Root relaxation solution time = 0.06 sec. (0.94 ticks)

        Nodes                                         Cuts/
   Node  Left     Objective  IInf  Best Integer    Best Bound    ItCnt     Gap

*     0+    0                          495.0000       56.0000            88.69%
Found incumbent of value 495.000000 after 0.44 sec. (77.77 ticks)
*     0+    0                          118.0000       56.0000            52.54%
Found incumbent of value 118.000000 after 0.44 sec. (78.15 ticks)
      0     0      111.7778    78      118.0000      111.7778      203    5.27%
*     0+    0                          116.0000      111.7778             3.64%
Found incumbent of value 116.000000 after 0.45 sec. (79.32 ticks)
      0     0      112.5000    78      116.0000      Cuts: 10      209    3.02%
*     0+    0                          113.0000      112.5000             0.44%
Found incumbent of value 113.000000 after 0.52 sec. (85.02 ticks)
      0     0        cutoff            113.0000      112.5000      209    0.44%
Elapsed time = 0.55 sec. (85.03 ticks, tree = 0.01 MB, solutions = 3)

Zero-half cuts applied:  5
Lift and project cuts applied:  2
Gomory fractional cuts applied:  3

Root node processing (before b&c):
  Real time             =    0.55 sec. (86.90 ticks)
Parallel b&c, 8 threads:
  Real time             =    0.00 sec. (0.00 ticks)
  Sync time (average)   =    0.00 sec.
  Wait time (average)   =    0.00 sec.
                          ------------
Total (root+branch&cut) =    0.55 sec. (86.90 ticks)
MIP status(101): integer optimal solution


We can see that the presolver is especially successful here: the model size is reduced by an extremely large amount. This will be less pronounced when the number of customers increases.

Solution


The solution looks like:



----     51 VARIABLE numStores.L           =          113  number of stores

----     51 VARIABLE placeStore.L  store locations

              j1          j6          j7          j8          j9         j15         j16         j17         j18

i4                                                                                                             1
i18                                                            1
i40                                                                                    1
i70            1
i79                                                                                                1
i80                        1
i107                                                                                   1
i118                                   1
i136                                                                       1
i157                                               1
i167                                                                       1
i193                                               1

   +         j21         j23         j26         j28         j29         j31         j32         j36         j38

i10                                    1
i28                                                                        1
i54                                                            1
i72                                                                                    1
i96                                                1
i113                       1
i147                                                           1
i158                                                                                                           1
i179                                                                                               1
i184                       1
i198           1

   +         j39         j44         j45         j46         j49         j50         j56         j58         j59

i5                                                                                                             1
i18                                                1
i39            1
i62                                                            1
i85                        1
i102                                                                       1
i104                                   1
i133                                               1
i166                                                                                               1
i195                                                                                   1

   +         j62         j66         j67         j68         j69         j73         j74         j76         j80

i11                                                                                                1
i16                                                1
i36                                                            1
i61                                                                                    1
i76                                                                        1
i105                                                                       1
i112           1
i117                                                                                                           1
i128                                   1
i146                                               1
i190                       1

   +         j82         j84         j85         j88         j90         j92         j95         j96         j97

i17                                    1
i26            1
i35                                                                                                1
i48                                    1
i68                                                                        1
i79                                                                                                            1
i97            1
i136                       1
i156                                               1
i170                                   1
i183                                                                                   1
i191                                                           1

   +         j98        j102        j107        j111        j112        j114        j115        j116        j118

i4                                                                         1
i22                                                                                    1
i36                                                            1
i56                        1
i63                                                1
i68                                                                                                            1
i88                                                                                                1
i100                                                                                   1
i101           1
i111                                                                                                           1
i129                                                           1
i140                                   1

   +        j119        j121        j126        j127        j132        j133        j134        j136        j139

i11                                                            1
i30                                    1
i53                                                1
i72                                                                                                1
i111                                                                                                           1
i129                                                                                   1
i144                                                                       1
i159           1
i183                                                                                               1
i191                       1

   +        j140        j147        j149        j150        j152        j153        j154        j156        j158

i14                                                            1
i35                        1
i48                                                                        1
i83                                                                                                1
i98                                                1
i117                                                                                                           1
i158                                                                                   1
i174                                   1
i194           1

   +        j161        j162        j163        j164        j166        j170        j172        j174        j175

i5                                                                         1
i32            1
i42                                                                                                1
i61                                                                                    1
i69                                                                        1
i103                                                           1
i143                                   1
i145                                                                                                           1
i158                                                                                               1
i192                       1
i198                                               1

   +        j176        j178        j179        j180        j182        j183        j184        j188        j191

i6                                                                         1
i13                                                                                                            1
i23                                                1
i47                                                                                                            1
i61                                                                                    1
i81                        1
i93            1
i103                                                                                                           1
i125                                   1
i182                                                           1
i193                                                                                               1

   +        j192        j193        j196

i73                                    1
i120           1
i138           1
i167                       1



We have really debunked that this is an intractable problem for a good MIP solver.

Note that the age of the universe is 13.7 billion years or 4.3e17 seconds. So we have achieved a speed-up of about 1e17. This is a record for me.

It is noted that this model does not really answer the question: what are the best locations for my stores? It only answers: how many stores do we need? In particular, it does not try to place stores as close as possible to customers. A subsequent optimization model can find good (or optimal) locations for the stores. Reference [2] discusses a similar issue for the continuous case, where facilities can be placed in any \((x,y)\) location instead of on a grid \((i,j)\).

Conclusion


The question: what is the minimum number of stores needed to service all customers can be answered by formulating and solving a set covering problem. This will give you locations for the stores. However, this model only looks at this objective, so no further attempts are undertaken to minimize the customer-store distances.

We can solve this problem for this particular problem quite efficiently using a standard MIP solver. Quite often I see that these solution approaches are dismissed, due to a misunderstanding of complexity-theoretic arguments. Complexity theory says very little about the performance of a particular MIP model using a particular solver on a given data set. It is better not to reject a MIP approach even before trying.

References


  1. Minimize number of shops while reaching all customers, https://stackoverflow.com/questions/62224042/minimize-number-of-shops-while-reaching-all-customers
  2. Solving a facility location problem as an MIQP, https://yetanothermathprogrammingconsultant.blogspot.com/2018/01/solving-facility-location-problem-as.html (this is essentially a continuous version of the current problem)

Monday, June 1, 2020

Simplify solutions

In the context of a real-world application, the client is interested to know about "simpler solutions". This makes the implementation of the solution an easier task. This sounds like a reasonable question. The modeling implications are not that trivial, however.

As a highly simplified example consider the solution:


----     32 VARIABLE x.L  patterns

            i1          i2          i3          i4          i5          i6          i7          i8

t1           1           4           3           2           1           1           2           4
t2                       3           5           3           5           4           1           3
t3           1           1           3           2           2           2           1           1
t4           3           4           1           3           4           2           1           3
t5           1           4           1           1           3           4           3           2
t6           2           1           2                       2           1           3           3
t7           4           1           3           4           3           1                       1


We see we have a different pattern for each time period \(t\). The question is: can we reduce the number of different patterns? Say, we want to allow just 4 different patterns.

Quadratic formulation


One way to model this is to have a pool of candidate patterns \(y_{c,i}\) with 4 candidates \(c\).  Then use a standard assignment-like construct to pick from those candidates;\[\begin{align}&x_{t,i} = \sum_c \mathit{select}_{t,c}\cdot y_{c,i}\\& \sum_c \mathit{select}_{t,c}=1&&\forall t\\ & \mathit{select}_{t,c} \in \{0,1\}\end{align}\] Unfortunately this is non-linear as both \(\mathit{select}_{t,c}\) and \(y_{c,i}\) are decision variables. We can solve this with a number of solvers such as global MINLP solvers (Baron, Couenne, Antigone), or global MIQCP solvers (Gurobi). The solution can look like:


----     49 VARIABLE y.L  candidate patterns

               i1          i2          i3          i4          i5          i6          i7          i8

cand1           3           4           1           3           4           2           1           3
cand2           1           4           2           1           2           1           3           3
cand3                       3           5           3           5           4           1           3
cand4           1           1           3           3           3           2           1           1


----     49 VARIABLE select.L  select from candidates

         cand1       cand2       cand3       cand4

t1                       1
t2                                   1
t3                                               1
t4           1
t5                       1
t6                       1
t7                                               1


----     49 VARIABLE x.L  patterns

            i1          i2          i3          i4          i5          i6          i7          i8

t1           1           4           2           1           2           1           3           3
t2                       3           5           3           5           4           1           3
t3           1           1           3           3           3           2           1           1
t4           3           4           1           3           4           2           1           3
t5           1           4           2           1           2           1           3           3
t6           1           4           2           1           2           1           3           3
t7           1           1           3           3           3           2           1           1


Indeed we see that the \(x\) variables show four different patterns. 

Note again that all these quantities (\(x\), \(y\), and \(\mathit{select}\)) are endogenous (i.e. determined by the solver).

Linearization


We can linearize this approach. First, we assume the variables \(x\) and \(y\) are bounded: \[x_{t,i}, y_{c,i} \in [0,U]\] We define \[sy_{t,c,i} =  \mathit{select}_{t,c}\cdot y_{c,i}\] With this we can formulate the linearization: \[\begin{align}& sy_{t,c,i} \le  \mathit{select}_{t,c} \cdot U \\ & sy_{t,c,i} \le y_{c,i} \\ & sy_{t,c,i} \ge y_{c,i} - U(1-\mathit{select}_{t,c}) \\ &x_{t,i} = \sum_c sy_{t,c,i} \\ &  sy_{t,c,i}  \in [0,U]\end{align}\]

The results look like:


----     62 VARIABLE y.L  candidate patterns

               i1          i2          i3          i4          i5          i6          i7          i8

cand1           1           4           2           1           2           1           3           3
cand2           1           1           3           3           3           2           1           1
cand3                       3           5           3           5           4           1           3
cand4           3           4           1           3           4           2           1           3


----     62 VARIABLE select.L  select from candidates

         cand1       cand2       cand3       cand4

t1           1
t2                                   1
t3                       1
t4                                               1
t5           1
t6           1
t7                       1


----     62 VARIABLE sy.L  sy=select*y

                  i1          i2          i3          i4          i5          i6          i7          i8

t1.cand1           1           4           2           1           2           1           3           3
t2.cand3                       3           5           3           5           4           1           3
t3.cand2           1           1           3           3           3           2           1           1
t4.cand4           3           4           1           3           4           2           1           3
t5.cand1           1           4           2           1           2           1           3           3
t6.cand1           1           4           2           1           2           1           3           3
t7.cand2           1           1           3           3           3           2           1           1


----     62 VARIABLE x.L  patterns

            i1          i2          i3          i4          i5          i6          i7          i8

t1           1           4           2           1           2           1           3           3
t2                       3           5           3           5           4           1           3
t3           1           1           3           3           3           2           1           1
t4           3           4           1           3           4           2           1           3
t5           1           4           2           1           2           1           3           3
t6           1           4           2           1           2           1           3           3
t7           1           1           3           3           3           2           1           1


This linearized formulation yields the same solution as the quadratic formulation shown earlier.

Notes:

  • The all-zero records in the \(sy\) variable are not printed.
  • For large problems with many candidate solutions, it may be advantageous to reduce symmetry in \(y\). One simple way would be to order solutions by their first index: \[ y_{c+1,'i1'} \ge y_{c,'i1'}\]
  • In my application, \(x\) has more dimensions. This does not really affect the approach.

Making the number of different patterns variable


Until now, we assumed that the maximum number of different patterns is fixed. In the above case, this was 4. What if we want to make it variable, so we can add it to the objective (with a certain weight) in order to minimize it? Well, we can use the \(\mathit{select}\) variable for that. We can write: \[\begin{align} & \mathit{patternUsed}_c \ge \mathit{select}_{t,c}&& \forall t,c\\ & \mathit{numPatternsUsed} = \sum_c \mathit{patternUsed}_c\end{align}\] Now, we can minimize \(\mathit{numPatternsUsed}\). Of course, we should make sure the set \(c\) is large enough (we can make it identical to the set \(t\)). To reduce symmetry, we can impose something like: \[ \mathit{patternUsed}_c  \le  \mathit{patternUsed}_{c+1}\] This will keep used patterns at the beginning. 

With this objective, we will try to find schedules with fewer unique patterns, when this is not too costly in terms of the other objectives.

Conclusion


This is a MIP formulation to keep track of the number of different, unique patterns. We can use it to simplify schedules and plans where it is beneficial to reduce the number of unique patterns. In my case, this was about creating simpler, less complex schedules that are easier to implement and communicate. The formulation is interesting, and not immediately obvious.