def solve_intermediate_objective( model: cp_model.CpModel, solver: cp_model.CpSolver, objective, hint=True, objective_type='min', callback: Optional[cp_model.CpSolverSolutionCallback] = None, alias=None, max_time=None, logger=logging): if max_time: solver.parameters.max_time_in_seconds = max_time if objective_type.lower().startswith('min'): model.Minimize(objective) objective_type = 'min' elif objective_type.lower().startswith('max'): model.Maximize(objective) objective_type = 'max' else: raise Exception(f'Can not "{objective_type}" objective') t0 = dt.datetime.now() if callback: status = solver.SolveWithSolutionCallback(model, callback) else: status = solver.Solve(model) duration = (dt.datetime.now() - t0).total_seconds() if status == cp_model.INFEASIBLE: logger.warning(f'INFEASIBLE solving {alias or objective}') return None, status elif status == cp_model.UNKNOWN: logger.warning(f'Time limit reached {alias or objective}') return None, status result = round(solver.ObjectiveValue()) if hint: hint_solution(model, solver) if not isinstance(objective, int): if status == cp_model.OPTIMAL: logger.debug( f'{alias or objective} == {result}, Seconds: {duration:.2f}') model.Add(objective == result) elif objective_type == 'min': logger.debug( f'{alias or objective} <= {result}, Seconds: {duration:.2f}') model.Add(objective <= result) elif objective_type == 'max': logger.debug( f'{alias or objective} >= {result}, Seconds: {duration:.2f}') model.Add(objective >= result) return result, status
def compute_optimal_bench_map( schedule: Schedule, all_employees: Dict[int, Employee], all_shifts: Dict[int, Shift], all_labs: Dict[str, Lab], all_benches: Dict[str, Bench], constraints: Constraints, ) -> Schedule: # Make sure employees have home_bench information for employee in all_employees: assert all_employees[employee].home_bench is not None model = CpModel() bench_placement = {} for shift in schedule.shift_schedule: for employee in schedule.shift_schedule[shift]: for bench in all_benches: bench_placement[( shift, employee.index, bench)] = model.NewBoolVar( f"bench_s{shift}e{employee.index}b{bench}") # 2 employees cannot have same bench for shift in schedule.shift_schedule: for bench in all_benches: model.Add( sum(bench_placement[(shift, employee.index, bench)] for employee in schedule.shift_schedule[shift]) <= 1) # All employees scheduled must have a bench for each shift # Other employees must not have a bench for shift in schedule.shift_schedule: for employee in schedule.shift_schedule[shift]: model.Add( sum(bench_placement[(shift, employee.index, bench)] for bench in all_benches) == 1) # Make sure appropriate benches are utilized during appropriate shift for shift in schedule.shift_schedule: for bench in all_benches: if (all_shifts[shift].label == "FULL") and (all_benches[bench].active_shift == "AM"): # This works because we want FULL shifts to use AM benches # If proper condition, no constraint will be added # If anything else, it will be added # For example, FULL + PM will have constraint == 0 pass # important elif (all_shifts[shift].label != all_benches[bench].active_shift): model.Add( sum(bench_placement[(shift, employee.index, bench)] for employee in schedule.shift_schedule[shift]) == 0) # Create objective # Minimize distance from home bench to placed bench model.Minimize( sum((( employee.home_bench.location.x # type: ignore - all_benches[bench].location.x)**2 + ( employee.home_bench.location.y # type: ignore - all_benches[bench].location.y)**2 + ( constraints.raw_lab_separation.loc[ # type: ignore employee.home_bench.lab.name, # type: ignore all_benches[bench].lab.name, ])) * bench_placement[(shift, employee.index, bench)] for shift in schedule.shift_schedule for employee in schedule.shift_schedule[shift] for bench in all_benches)) st.write("Running Location Optimization...") solver = CpSolver() solver.parameters.max_time_in_seconds = 60 solver.Solve(model) st.write("Finished Location Optimization!") shift_bench_schedule: Dict[int, Dict[int, Bench]] = { } # {Shift.id: {Employee.id: Bench}} shift_bench_schedule = { shift: { employee.index: all_benches[bench] for employee in schedule.shift_schedule[shift] for bench in all_benches if solver.Value(bench_placement[(shift, employee.index, bench)]) } for shift in all_shifts } employee_bench_schedule: Dict[int, Dict[int, Bench]] = { } # {Employee.id: {Shift.id: Bench}} for shift in shift_bench_schedule: for employee in shift_bench_schedule[shift]: employee_bench_schedule[employee] = {} for shift in shift_bench_schedule: for employee in shift_bench_schedule[shift]: employee_bench_schedule[employee][shift] = shift_bench_schedule[ shift][employee] employee_bench_distances: Dict[int, float] = { } # {Employee.id: average distance} for employee in employee_bench_schedule: employee_bench_distances[employee] = statistics.mean([ ((employee_bench_schedule[employee][shift].location.x - all_employees[employee].home_bench.location.x)**2 + (employee_bench_schedule[employee][shift].location.y - all_employees[employee].home_bench.location.y)**2)**0.5 for shift in employee_bench_schedule[employee] ]) schedule.shift_bench_schedule = shift_bench_schedule schedule.employee_bench_schedule = employee_bench_schedule schedule.bench_objective_score = solver.ObjectiveValue() schedule.bench_optimization_average_distance = employee_bench_distances return schedule
def compute_optimal_schedule( all_employees: Dict[int, Employee], all_shifts: Dict[int, Shift], all_days: Dict[str, Day], schedule_parameters: ConstraintParameters, ) -> Schedule: model = CpModel() shifts = {} for employee in all_employees: for shift in all_shifts: shifts[(employee, shift)] = model.NewBoolVar(f"shift_e{employee}s{shift}") # Each shift has max number of people # Up to input to calculate AM + FULL and PM + FULL true constraints for shift in all_shifts: model.Add( sum(shifts[(employee, shift)] for employee in all_employees) <= all_shifts[shift].max_capacity) # Each person has max slots for employee in all_employees: model.Add( sum(shifts[(employee, shift)] if all_shifts[shift].label != "FULL" else 2 * shifts[(employee, shift)] for shift in all_shifts) <= all_employees[employee].max_slots) # A person can only have one of AM, PM, or FULL per day for employee in all_employees: for day in all_days: model.Add( sum(shifts[(employee, shift)] for shift in all_shifts if all_shifts[shift].day == day) <= 1) # A person might not be able to work all possible shifts for employee in all_employees: for shift in all_shifts: if all_employees[employee].preferences[shift] <= -100: model.Add(sum([shifts[(employee, shift)]]) < 1) # Workaround for minimizing variance of scores # Instead, minimize difference between max score and min score # From: https://stackoverflow.com/a/53363585 employee_scores = {} for employee in all_employees: employee_scores[employee] = model.NewIntVar( -100, 100, f"employee_score_{employee}") model.Add(employee_scores[employee] == sum( all_employees[employee].preferences[shift] * shifts[(employee, shift)] if all_shifts[shift].label != "FULL" else 2 * all_employees[employee].preferences[shift] * shifts[(employee, shift)] for shift in all_shifts)) min_employee_score = model.NewIntVar(-100, 100, "min_employee_score") max_employee_score = model.NewIntVar(-100, 100, "max_employee_score") model.AddMinEquality(min_employee_score, [employee_scores[e] for e in all_employees]) model.AddMaxEquality(max_employee_score, [employee_scores[e] for e in all_employees]) # Max Unfairness constraint if schedule_parameters.max_unfairness >= 0: model.Add(max_employee_score - min_employee_score <= schedule_parameters.max_unfairness) # Create objective # Maximize points from requests # Maximize number of shifts filled # Minimize variance of scores per employee weights: Dict[str, int] = { "preferences": int(10 * (1 / (1 + schedule_parameters.fill_schedule_weight + schedule_parameters.fairness_weight))), "fill_schedule": int(10 * (schedule_parameters.fill_schedule_weight / (1 + schedule_parameters.fill_schedule_weight + schedule_parameters.fairness_weight))), "fairness": int(10 * (schedule_parameters.fairness_weight / (1 + schedule_parameters.fill_schedule_weight + schedule_parameters.fairness_weight))), } model.Maximize(weights["preferences"] * sum( all_employees[employee].preferences[shift] * shifts[ (employee, shift)] if all_shifts[shift].label != "FULL" else 2 * all_employees[employee].preferences[shift] * shifts[(employee, shift)] for employee in all_employees for shift in all_shifts) + weights["fill_schedule"] * sum(shifts[(employee, shift)] for employee in all_employees for shift in all_shifts) - weights["fairness"] * (max_employee_score - min_employee_score)) st.write("Constraints set up. Running Optimization...") solver = CpSolver() solver.parameters.max_time_in_seconds = 60 solver.Solve(model) st.write("Finished Schedule Optimization!") # Prepare results shifts_per_employee: Dict[int, int] = { employee: sum( solver.Value(shifts[( employee, shift)]) if all_shifts[shift].label != "FULL" else 2 * solver.Value(shifts[(employee, shift)]) for shift in all_shifts) for employee in all_employees } score_per_employee: Dict[int, int] = { employee: sum(all_employees[employee].preferences[shift] * solver.Value(shifts[ (employee, shift)]) if all_shifts[shift].label != "FULL" else 2 * all_employees[employee].preferences[shift] * solver.Value(shifts[(employee, shift)]) for shift in all_shifts) for employee in all_employees } score_variance: float = statistics.variance(score_per_employee.values()) total_possible_shifts: int = sum( all_shifts[shift]. max_capacity if all_shifts[shift].label != "FULL" else 2 * all_shifts[shift].max_capacity for shift in all_shifts) employee_schedule: Dict[int, List[Shift]] = { employee: [ all_shifts[shift] for shift in all_shifts if solver.Value(shifts[(employee, shift)]) ] for employee in all_employees } shift_schedule: Dict[int, List[Employee]] = { shift: [ all_employees[employee] for employee in all_employees if solver.Value(shifts[(employee, shift)]) ] for shift in all_shifts } schedule: Schedule = Schedule( objective_score=solver.ObjectiveValue(), time_to_solve=solver.WallTime(), number_of_shifts_per_employee=shifts_per_employee, score_per_employee=score_per_employee, score_variance=score_variance, min_employee_score=solver.Value(min_employee_score), max_employee_score=solver.Value(max_employee_score), total_possible_shifts=total_possible_shifts, total_shifts_filled=sum(shifts_per_employee.values()), employee_schedule=employee_schedule, shift_schedule=shift_schedule, ) return schedule