Title: How to use Pulp to set up conditional statements and constraints in Python?
Tags: python,conditional-statements,constraints,pulp

Question: I'm trying to use Pulp to create a shift scheduling tool with various constraints on when an employee can work and how many of each shift they can work. An example of the input parameters are as follows:
Inputs:

Total Staff= 8
Special Staff = 4
Days = 30
Shifts = 7

Variables:

shift_0 (day)
shift_1 (day)
shift_2 (special day)
shift_3 (special night)
shift_4 (night)
shift_5 (optional night)
shift_6 (vacation)

Constraints:

A worker can only work one shift a day
Shifts 0, 1, 4 must be filled everyday
Special staff can't work more than 2 'Thursdays' (Days 3, 10, 17, 24)
Each shift can only be filled by one person
Each worker must work exactly 5 nights (shifts 4 or 5)
All nights are scheduled consecutively (i.e., 5 days of nights in a row)
No worker may work more than 12 days consecutively
All night shift blocks must be preceded by a special night and followed by 2 days vacation

I have several constraints that I'm working through but the few that I'm primarily hung up on are that night shifts must be scheduled in consecutive days (5 at a time), a 'special night' shift has to precede a block of nights, and two vacation days must follow a block of nights.
I'm using conditional if-else statements to setup the problem but I'm not getting the constraint I expect when I look at the LP file. I get a 'Status Infeasible' error when executing the code. If I ignore the block in question then a solution is found.
Any suggestions on how I can better cast the constraints?
import pulp

number_of_staff = 8  #i
number_of_rotators = 4 #z
number_of_days = 28  #j
number_of_shifts = 7 #k Two day, one day admit, one PM admit, two night (one optional), one vacation (optional)

# Decision Variables
var_day0 = pulp.LpVariable.dicts('day-0', (range(number_of_staff), range(number_of_days)), 0, 1, 'Binary')
var_day1 = pulp.LpVariable.dicts('day-1', (range(number_of_staff), range(number_of_days)), 0, 1, 'Binary')
var_day_admit = pulp.LpVariable.dicts('day-admit', (range(number_of_staff), range(number_of_days)), 0, 1, 'Binary')
var_pm_admit = pulp.LpVariable.dicts('pm-admit', (range(number_of_staff), range(number_of_days)), 0, 1, 'Binary')
var_night0 = pulp.LpVariable.dicts('night-0', (range(number_of_staff), range(number_of_days)), 0, 1, 'Binary')
var_night1 = pulp.LpVariable.dicts('night-1', (range(number_of_staff), range(number_of_days)), 0, 1, 'Binary')
var_off = pulp.LpVariable.dicts('off', (range(number_of_staff), range(number_of_days)), 0, 1, 'Binary')

# Object function
obj = None
weekend_weight = 0

#Preference that all shifts are staffed
for i in range(number_of_staff):
    for j in range(number_of_days):
        obj += var_day0[i][j] + var_day1[i][j] + var_day_admit[i][j] + var_pm_admit[i][j] + var_night0[i][j]
    
    #Add bonus for consecutive weekend days off
    if var_off[i][5] == 1 and var_off[i][6] == 1:
        obj += weekend_weight
        
    if var_off[i][12] == 1 and var_off[i][13] == 1:
        obj += weekend_weight
        
    if var_off[i][19] == 1 and var_off[i][20] == 1:
        obj += weekend_weight
        
    if var_off[i][26] == 1 and var_off[i][27] == 1:
        obj += weekend_weight
        
problem = pulp.LpProblem('shift', pulp.LpMaximize)
problem += obj

# # Setting constraints

# Both day shifts must be filled, night shift must be filled
for j in range(number_of_days):
    c = None
    d = None
    e = None
    
    for i in range(number_of_staff):
        c += var_day0[i][j]
        d += var_day1[i][j]
        e += var_night0[i][j]
        
    problem += c == 1
    problem += d == 1
    problem += e == 1

# No more than one person on optional night
for j in range(number_of_days):
    c = None
    
    for i in range(number_of_staff):
        c += var_night1[i][j]
        
    problem += c <= 1
    
# Only one shift a day
for j in range(number_of_days):
    
    for i in range(number_of_staff):
        c = None
        c += var_day0[i][j] + var_day1[i][j] + var_day_admit[i][j] + var_pm_admit[i][j] + var_night0[i][j] + var_night1[i][j] + var_off[i][j]
        
        problem += c == 1
        
# Everyone must work 5 nights
for i in range(number_of_staff):
    c = None
    
    for j in range(number_of_days):
        c += var_night0[i][j] + var_night1[i][j]
        
    problem += c == 5
    
# No one can work more than 12 days in a row      
for i in range(number_of_staff):
    for j in range(0, number_of_days-13):
        c = None
        
        for w in range(j, j+12):
            c += var_day0[i][j] + var_day1[i][j] + var_day_admit[i][j] + var_pm_admit[i][j] + var_night0[i][j] + var_night1[i][j]
        
        problem += c <= 12
        
# Rotators no more than 2 thursdays of night or pm
for z in range(number_of_rotators):
    c = None
    for j in [3, 10, 17, 24]:
        c += var_night0[z][j] + var_night1[z][j] + var_pm_admit[z][j]
        
    problem += c <= 2
    
# All nights must be 5 consecutive shifts and must be preceded by a pm_admit and followed by two days off
##### PROBLEM SECTION BELOW #####
for i in range(number_of_staff):
    c = None
    d = None
    e = None
    
    for j in range(number_of_days):
             
        if var_night0[i][j] == 1 or var_night1[i][j] == 1:
            
            for w in range(5):
                c += var_night0[i][j+w] + var_night1[i][j+w]
            
            if j != 0:
                d += var_pm_admit[i][j-1]
            else:
                d = 1
                
            if j < 23:
                e += var_off[i][j+5] + var_off[i][j+6]
            else:
                e = 2
            
            break
            
    problem += c == 5
    problem += d == 1
    problem += e == 2

# Solve problem
status = problem.solve()

# If optimal print 'Optimal solution found'
problem.writeLP('lpfile')

print('Status', pulp.LpStatus[status])

Answer: For anyone still wondering, I was able to resolve my issue by switching the 'night' variable to only be 1 on the first night shift of the 5 day block. It made the other constraints a bit more clunky as result, but it does linearize the problem.