0

For a work shift optimization problem, I've defined a binary variable in PuLP as follows:

pulp.LpVariable.dicts('VAR', (range(D), range(N), range(T)), 0, 1, 'Binary')

where

  1. D = # days in each schedule we create (=28, or 4 weeks)
  2. N = # of workers
  3. T = types of work shift (=6)

For the 5th and 6th type of work shift (with index 4 and 5), I need to add a constraint that any worker who works these shifts must do so for seven consecutive days... and not any seven days but the seven days starting from Monday (aka a full week). I've tried defining the constraint as follows, but I'm getting an infeasible solution when I add this constraint and try to solve the problem (it worked before without it)

I know this constraint (along with the others from before) should theoretically be feasible because we manually schedule work shifts with the same set of constraints. Is there anything wrong with the way I've coded the constraint?

## looping over each worker
for j in range(N):
    ## looping for every Monday in the 28 days 
    for i in range(0,D,7):
        c = None
        ## accessing only the 5th and 6th work shift type 
        for k in range(4,T):
            c+=var[i][j][k]+var[i+1][j][k]+var[i+2][j][k]+var[i+3][j][k]+var[i+4][j][k]+var[i+5][j][k]+var[i+6][j][k]
        problem+= c==7
Daniel Junglas
  • 5,830
  • 1
  • 5
  • 22
Jason
  • 13
  • 2

2 Answers2

1

If I understand correctly then your constraint requires that each worker is required to work the 4th and 5th shift in every week. This is because of c == 7, i.e. 7 of the binaries in c must be set to 1. This does not allow any worker to work in shift 0 through 3, right?

You need to change the constraint so that c == 7 is only enforced if the worker works any shift in that range. A very simple way to do that would be something like

v = list()
for k in range(4,T):
  v.extend([var[i][j][k], var[i+1][j][k], var[i+2][j][k], var[i+3][j][k], var[i+4][j][k], var[i+5][j][k], var[i+6][j][k]])
c = sum(v)
problem += c <= 7        # we can pick at most 7 variables from v
for x in v:
  problem += 7 * x <= c  # if any variable in v is picked, then we must pick 7 of them

This is by no means the best way to model that (indicator variables would be much better), but it should give you an idea what to do.

Daniel Junglas
  • 5,830
  • 1
  • 5
  • 22
  • thanks for the response, appreciate it! maybe i didn't understand your code properly, but the constraint is closer to (removing the 4th and 5th shift caveat for clarity): for any worker who works shift A, they must do so for the whole week (a.k.a var[i][j][k], var[i+1][j][k], ..., var[i+6][j][k] must all equal 1 for k = A if any were to be = 1 in the first place for person j). not sure what the list extension is doing. would appreciate further clarity! – Jason Jan 06 '22 at 13:15
  • hey, I tried implementing the solution and with a few changes it worked marvels! still would love to know why though ahah – Jason Jan 06 '22 at 13:47
  • Ok, with your updated description, I think you just have to move everything into the `k` loop: 1. Construct the list `v` of all variables that correspond to shift A, the current worker and the current week. 2. Compute `c = sum(v)`. 3. Add `problem += c <= 7`. 4. Add constraints for each variable in `v`: `for x in v: problem += 7 * x <= c`. I think that should state your constraints on a per-shift basis. The trick is `7 * x <= c`: If at least one `x` variable is 1 (i.e., worker works shift A in this week), then all variables in `c` are forced to 1. – Daniel Junglas Jan 07 '22 at 15:17
0

Just to offer an alternative approach, assuming (as I read it) that for any given week a worker can either work some combination of shifts in [0:3] across the seven days, or one of the shifts [4:5] every day: we can do this by defining a new binary variable Y[w][n][t] which is 1 if in week w worker n does a restricted shift t, 0 otherwise. Then we can relate this variable to our existing variable X by adding constraints so that the values X can take depend on the values of Y.

# Define the sets of shifts
non_restricted_shifts = [0,1,2,3]
restricted_shifts = [4,5]

# Define a binary variable Y, 1 if for week w worker n works restricted shift t
Y = LpVariable.dicts('Y', (range(round(D/7)), range(N), restricted_shifts), cat=LpBinary)

# If sum(Y[week][n][:]) = 1, the total number of non-restricted shifts for that week and n must be 0
for week in range(round(D/7)):
    for n in range(N):
        prob += lpSum(X[d][n][t] for d in range(week*7, week*7 + 7) for t in non_restricted_shifts) <= 1000*(1-lpSum(Y[week][n][t] for t in restricted_shifts))

# If worker n has 7 restricted shift t in week w, then Y[week][n][t] == 1, otherwise it is 0
for week in range(round(D/7)):
    for n in range(N):
        for t in restricted_shifts:
            prob += lpSum(X[d][n][t] for d in range(week*7, week*7+7)) <= 7*(Y[week][n][t]) 
            prob += lpSum(X[d][n][t] for d in range(week*7, week*7+7)) >= Y[week][n][t]*7

Some example output (D=14, N=2, T=6):

        / M T W T F S S / M T W T F S S / M T W T F S S / M T W T F S S
WORKER 0
Shifts: / 2 3 1 3 3 2 2 / 1 0 2 3 2 2 0 / 3 1 2 2 3 1 1 / 2 3 0 3 3 0 3
WORKER 1
Shifts: / 3 1 2 3 1 1 2 / 3 3 2 3 3 3 3 / 4 4 4 4 4 4 4 / 1 3 2 2 3 2 1
WORKER 2
Shifts: / 1 2 3 1 3 1 1 / 3 3 2 2 3 2 3 / 3 2 3 0 3 1 0 / 4 4 4 4 4 4 4
WORKER 3
Shifts: / 2 2 3 2 1 2 3 / 5 5 5 5 5 5 5 / 3 1 3 1 0 3 1 / 2 2 2 2 3 0 3
WORKER 4
Shifts: / 5 5 5 5 5 5 5 / 3 3 1 0 2 3 3 / 0 3 3 3 3 0 2 / 3 3 3 2 3 2 3
borderline27
  • 116
  • 6