def _solution_to_graph(self, solver: cp_model.CpSolver) -> Graph: result: Graph = self._graph for key in self._n_vars: result.set_node_label(key, str(solver.Value(self._n_vars[key]))) for key in self._e_vars: result.set_edge_label(key, str(solver.Value(self._e_vars[key]))) return result
def get_solution_assignments( self, *, solver: cp_model.CpSolver, items: Tuple[Candidate, ...] ) -> ConstraintSolutionSectionSet: """ Using a solver that has already solved for the overall constraints, create a constraint solution section set that captures all the sections, the items assigned to each section, and the scores/attribute values associated with those assignments :param solver: A constraint solver, which has already been run to obtain a solution :param items: A tuple of candidates that was used by the solver to derive the solution :return: A ConstraintSolutionSectionSet object capturing the relevant information for this set of sections """ section_scores = [0 for s in self._sections] section_attribute_values = [ {attr: 0 for attr in self._attributes_of_interest} for s in self._sections ] section_items = [[] for s in self._sections] for i in range(len(items)): for j in range(len(self._sections)): if solver.Value(self._item_assignments[i, j]): section_items[j].append(items[i]) section_scores[j] += items[i].total_score for attr in self._attributes_of_interest: section_attribute_values[j][attr] += rgetattr( items[i].domain_object, attr ) return ConstraintSolutionSectionSet( sections=tuple( ConstraintSolutionSection( section_object=self._sections[j], section_score=section_scores[j], section_attribute_values=section_attribute_values[j], section_candidates=tuple(section_items[j]), ) for j in range(len(self._sections)) ) )
def optimize( self, input_params: InputParameters, timing_object: TimingData) -> Tuple[Testcase, EOptimizationStatus]: solver = CpSolver() solver.parameters.max_time_in_seconds = input_params.timeouts.timeout_pint print_model_stats(self.model.ModelStats()) t = Timer() with t: solver_status = solver.Solve(self.model) timing_object.time_optimizing_pint = t.elapsed_time Pint = -1 status = EOptimizationStatus.INFEASIBLE if solver_status == OPTIMAL: status = EOptimizationStatus.OPTIMAL elif solver_status == FEASIBLE: status = EOptimizationStatus.FEASIBLE elif solver_status == INFEASIBLE: status = EOptimizationStatus.INFEASIBLE elif solver_status == UNKNOWN: status = EOptimizationStatus.UNKNOW elif solver_status == MODEL_INVALID: status = EOptimizationStatus.MODEL_INVALID if (status == EOptimizationStatus.FEASIBLE or status == EOptimizationStatus.OPTIMAL): r = solver.Value(self.Pint_var) Pint = r else: raise ValueError( "CPSolver returned invalid status for Pint model: " + str(status)) self.tc.Pint = Pint return self.tc, status
def get_solved_values(solver: CpSolver, letters: List) -> List[int]: return list(solver.Value(letter) for letter in letters)
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