0

I've copied a code example that works fine by itself, and does work in my version as well. The problem is after asking the user the number of employees, shifts, and days being scheduled, the program won't display the results of the solver. It will do this for the example code alone, but not with the code I have written to control it. If anyone can take a look at this to see why, it would be greatly appreciated.

from ortools.sat.python import cp_model

def sup_functions(): # supervisor functions
    sup_task = input('I want to: ')   
    # scheduling employees
    if sup_task == 'schedule employees':
        class EmpsPartialSolutionPrinter(cp_model.CpSolverSolutionCallback):
            
            def __init__(self, shifts, num_emps, num_days, num_shifts, sols):
                cp_model.CpSolverSolutionCallback.__init__(self)
                self._shifts = shifts
                self._num_emps = num_emps
                self._num_days = num_days
                self._num_shifts = num_shifts
                self._solutions = set(sols)
                self._solution_count = 1
            def on_solution_callback(self):
                if self._solution_count in self._solutions:
                    print('Solution %i' % self._solution_count)
                    for d in range(self._num_days):
                        print('Day %i' % d)
                        for n in range(self._num_emps):
                            is_working = False
                            for s in range(self._num_shifts):
                                if self.Value(self._shifts[(n, d, s)]):
                                    is_working = True
                                    print('  Employee %i works shift %i' % (n, s))
                                if not is_working:
                                    print('  Employee {} does not work'.format(n))
                    print()
                self._solution_count += 1
            
            def solution_count(self):
                return self._solution_count
        
        def main():
            # Data.
            num_emps = int(input("How many employees are you scheduling? "))
            num_days = int(input("How many days are you scheduling for? "))
            num_shifts = int(input(f"How many shifts are you scheduling for each employees for {num_days} days? "))
            all_emps = range(num_emps)
            all_shifts = range(num_shifts)
            all_days = range(num_days)
            # Creates the model.
            model = cp_model.CpModel()

            # Creates shift variables.
            # shifts[(n, d, s)]: nurse 'n' works shift 's' on day 'd'.
            shifts = {}
            for n in all_emps:
                for d in all_days:
                    for s in all_shifts:
                        shifts[(n, d,
                                s)] = model.NewBoolVar('shift_n%id%is%i' % (n, d, s))

            # Each shift is assigned to exactly one employee in the schedule period.
            for d in all_days:
                for s in all_shifts:
                    model.Add(sum(shifts[(n, d, s)] for n in all_emps) == 1)

            # Each emmployee works at most one shift per day.
            for n in all_emps:
                for d in all_days:
                    model.Add(sum(shifts[(n, d, s)] for s in all_shifts) <= 1)

            # Try to distribute the shifts evenly, so that each employee works
            # min_shifts_per_emp shifts. If this is not possible, because the total
            # number of shifts is not divisible by the number of employees, some employees will
            # be assigned one more shift.
            min_shifts_per_emp = (num_shifts * num_days) // num_emps
            if num_shifts * num_days % num_emps == 0:
                max_shifts_per_emp = min_shifts_per_emp
            else:
                max_shifts_per_emp = min_shifts_per_emp + 1
            for n in all_emps:
                num_shifts_worked = 0
                for d in all_days:
                    for s in all_shifts:
                        num_shifts_worked += shifts[(n, d, s)]
                    model.Add(min_shifts_per_emp <= num_shifts_worked)
                    model.Add(num_shifts_worked <= max_shifts_per_emp)

            # Creates the solver and solve.
            solver = cp_model.CpSolver()
            solver.parameters.linearization_level = 0
            # Display the first five solutions.
            a_few_solutions = range(5)
            solution_printer = EmpsPartialSolutionPrinter(shifts, num_emps,
                                                    num_days, num_shifts,
                                                    a_few_solutions)
            solver.SearchForAllSolutions(model, solution_printer)
        if __name__ == '__main__':
            main()

# lets program know which functions to call
def emporsup():
    emp_or_sup = input('Are you an employee or supervisor? ')  # determines if user is an employee or the owner
    if emp_or_sup == "supervisor":
        sup_functions()
    #elif emp_or_sup == "employee":
        #emp_functions()
    else:
        print("not a valid response")
        emporsup()
emporsup()

CrazyChucky
  • 3,263
  • 4
  • 11
  • 25
  • 1
    Is the indentation of your code displayed accurately? It looks like you're defining your class, defining your `main` function, and calling `main` all inside an `if`, which I doubt is what you want. The `__name__ == '__main__'` check is usually used at top level, to define what code is executed when you run your script. See [here](https://stackoverflow.com/questions/419163/what-does-if-name-main-do) for more detailed info. – CrazyChucky Dec 19 '20 at 15:47
  • It prints the results if it's able to solve. Try 1 employee, 1 day, 1 shift, and see that it prints output. It prints nothing when it can't find a solution. It seems like your solution-finding code is wrong, but I'm not familiar enough with this module to tell you how. Either way, you should really define your class and functions at the top level (not indented). There are valid reasons to dynamically define such things, but this really isn't one of them. Define them first, then let your control structure decide what to call when. – CrazyChucky Dec 19 '20 at 16:30
  • Thanks for the input. The code works fine alone but adding the if statement is what prevents the module from printing solutions –  Dec 19 '20 at 16:33
  • And to answer your question, the program printed this when I tried the 1 employee, 1 day, and 1 shift: Solution 1 Day 0 Employee 0 works shift 0 –  Dec 19 '20 at 16:35

1 Answers1

0

Past a certain point, it's better to try a cleaner approach than to pinpoint where precisely the current one is going wrong. Here's a stab at a better way to structure what you're doing, and it appears to work correctly.

First, the code copied from the example, with "nurses" replaced by "employees" and the static assignments changed to user input, as you've done. I've also renamed main to schedule_employees.

from ortools.sat.python import cp_model

class EmpsPartialSolutionPrinter(cp_model.CpSolverSolutionCallback):
    """Print intermediate solutions."""

    def __init__(self, shifts, num_employees, num_days, num_shifts, sols):
        cp_model.CpSolverSolutionCallback.__init__(self)
        self._shifts = shifts
        self._num_employees = num_employees
        self._num_days = num_days
        self._num_shifts = num_shifts
        self._solutions = set(sols)
        self._solution_count = 0

    def on_solution_callback(self):
        if self._solution_count in self._solutions:
            print('Solution %i' % self._solution_count)
            for d in range(self._num_days):
                print('Day %i' % d)
                for n in range(self._num_employees):
                    is_working = False
                    for s in range(self._num_shifts):
                        if self.Value(self._shifts[(n, d, s)]):
                            is_working = True
                            print('  Nurse %i works shift %i' % (n, s))
                    if not is_working:
                        print('  Nurse {} does not work'.format(n))
            print()
        self._solution_count += 1

    def solution_count(self):
        return self._solution_count

def schedule_employees():
    # Data.
    num_employees = int(input("How many employees are you scheduling? "))
    num_days = int(input("How many days are you scheduling for? "))
    num_shifts = int(input("How many shifts are you scheduling for each "
                          f"employees for {num_days} days? "))
    all_employees = range(num_employees)
    all_shifts = range(num_shifts)
    all_days = range(num_days)
    # Creates the model.
    model = cp_model.CpModel()

    # Creates shift variables.
    # shifts[(n, d, s)]: nurse 'n' works shift 's' on day 'd'.
    shifts = {}
    for n in all_employees:
        for d in all_days:
            for s in all_shifts:
                shifts[(n, d,
                        s)] = model.NewBoolVar('shift_n%id%is%i' % (n, d, s))

    # Each shift is assigned to exactly one nurse in the schedule period.
    for d in all_days:
        for s in all_shifts:
            model.Add(sum(shifts[(n, d, s)] for n in all_employees) == 1)

    # Each nurse works at most one shift per day.
    for n in all_employees:
        for d in all_days:
            model.Add(sum(shifts[(n, d, s)] for s in all_shifts) <= 1)

    # Try to distribute the shifts evenly, so that each nurse works
    # min_shifts_per_nurse shifts. If this is not possible, because the total
    # number of shifts is not divisible by the number of employees, some employees will
    # be assigned one more shift.
    min_shifts_per_nurse = (num_shifts * num_days) // num_employees
    if num_shifts * num_days % num_employees == 0:
        max_shifts_per_nurse = min_shifts_per_nurse
    else:
        max_shifts_per_nurse = min_shifts_per_nurse + 1
    for n in all_employees:
        num_shifts_worked = 0
        for d in all_days:
            for s in all_shifts:
                num_shifts_worked += shifts[(n, d, s)]
        model.Add(min_shifts_per_nurse <= num_shifts_worked)
        model.Add(num_shifts_worked <= max_shifts_per_nurse)

    # Creates the solver and solve.
    solver = cp_model.CpSolver()
    solver.parameters.linearization_level = 0
    # Display the first five solutions.
    a_few_solutions = range(5)
    solution_printer = EmpsPartialSolutionPrinter(shifts, num_employees,
                                                    num_days, num_shifts,
                                                    a_few_solutions)
    solver.SearchForAllSolutions(model, solution_printer)

This, more or less, is all you need to add/change. The top level entry point -- basically, the stuff you want to happen first when you execute your script -- goes inside the __name__ == '__main__' check, just like the example did it. The difference, of course, is that we're performing a couple of checks prior to calling what used to simply be the main method.

if __name__ == '__main__':
    valid_roles = ['supervisor', 'employee']
    
    role = input('Are you an employee or supervisor? ')
    while role not in valid_roles:
        print('not a valid response')
        role = input('Are you an employee or supervisor? ')
    
    task = input('I want to: ')
    if role == 'supervisor' and task == 'schedule employees':
        schedule_employees()

If you add to this program and these checks become more complex, you might consider bundling them off into your own new main method, or even making a separate get_role method for example, but for now this is short and simple enough that it's fine here.

The basic rule is: define your classes and functions and such first, at the top level, and then use whatever control flow logic you want in order to decide what to call, how, and when. There are reasons you might eventually want to define classes inside conditionals and/or functions, but it's pretty unusual and for complex situations.

CrazyChucky
  • 3,263
  • 4
  • 11
  • 25
  • Thanks for this, I'm currently reading over it to fully understand everything you did. I did a shortened version of this code I posted, I actually have around 70 lines of code above the code I posted when I asked the question. I'm hoping it won't affect anything too much. These codes always seem to work fine individually until it's time to combine. Thanks again for taking time to assist me. –  Dec 19 '20 at 16:56
  • @antongoldsmith I was just thinking about this again, and I think it might be helpful for you to read up some more on the [basics of functions](http://anh.cs.luc.edu/python/hands-on/3.1/handsonHtml/functions.html), to get a better feel for the distinction between *defining* and *calling*. Hopefully, with a better sense of "why", thinking about "how" will become more natural for you. – CrazyChucky Dec 20 '20 at 17:54
  • You're definitely right @CrazyChucky. I recently got into programming after I took the class in college last semester. I know the basics, but I've spent all of last month and this month practicing projects of my own. Learning the "why" of functions would be beneficial versus just knowing how they all work. I can say I know how most functions work, but not necessarily why they work, why I need them, etc. I started back in September, so I feel I've been learning quickly, but I know I have much more to learn. –  Dec 21 '20 at 18:15