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 optimize( self, input_params, timing_object: TimingData) -> Tuple[Testcase, EOptimizationStatus]: solver = CpSolver() solver.parameters.max_time_in_seconds = input_params.timeouts.timeout_scheduling print_model_stats(self.model.ModelStats()) t = Timer() try: with t: for l_or_n_id in itertools.chain(self.tc.L.keys(), self.tc.N.keys()): self.model.AddDecisionStrategy( list(self.o_f[l_or_n_id].values()), cp_model.CHOOSE_LOWEST_MIN, cp_model.SELECT_MIN_VALUE, ) self.model.AddDecisionStrategy( list(self.o_t.values()), cp_model.CHOOSE_LOWEST_MIN, cp_model.SELECT_MIN_VALUE, ) self.model.AddDecisionStrategy( list(self.phi_f.values()), cp_model.CHOOSE_LOWEST_MIN, cp_model.SELECT_MIN_VALUE, ) solver_status = solver.Solve(self.model) # print("Solved!\n{}".format(solver.ResponseStats())) except Exception as e: report_exception(e) solver_status = -1 timing_object.time_optimizing_scheduling = t.elapsed_time 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): schdl = schedule.from_cp_solver(solver, self, self.tc) self.tc.add_to_datastructures(schdl) return self.tc, status else: report_exception( "CPSolver returned invalid status for scheduling model: " + str(status)) return self.tc, status
def __init__(self): self.model = CpModel() self.solver = CpSolver() # Ideal number of workers is 8, see https://or.stackexchange.com/a/4126 self.solver.parameters.num_search_workers = 8 self._known_names = set() self._vars_bool = {} self._vars_int = {} self._vars_weighted = [] self._vars_weighted_cost = [] self.printer = ObjectiveSolutionPrinter()
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 __init__(self, num_queens: int, printer: bool): # Initialize model and solver self._cp_model = CpModel() self._cp_solver = CpSolver() self._num_queens = num_queens self._indices = range(num_queens) # Initialize the board self._board = self._initialize_board() # Add constraint for exactly 1 queen in each row, col self._constrain_rows_and_columns() # Add constraint for at most 1 queen in each diagonal self.constrain_diagonal(self.backwards_diagonal_func()) self.constrain_diagonal(self.forwards_diagonal_func()) # Add constraint for exactly N queens on board self._constrain_num_queens() # initialize solution printer self._solution_printer = NQueensPrinter(self._board, printer)
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 print_status(solver: CpSolver, letters: List, status: Any) -> None: if solver.StatusName(status) == "OPTIMAL": [s, e, n, d, m, o, r, y] = get_solved_values(solver, letters) send: str = f"{s} {e} {n} {d}" more: str = f"{m} {o} {r} {e}" money: str = f"{m} {o} {n} {e} {y}" print("<Problem>") print(" S E N D") print("+ ) M O R E") print("-----------") print(" M O N E Y") print("\n===========\n") print("<Answer>") print(f" {send}") print(f"+ ) {more}") print("-----------") print(f" {money}")
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)) ) )
class WiTiProblem: wt_model = CpModel() wt_solver = CpSolver() tasks = [] tasks_nb = 0 """ Metoda load_from_file służy do załadowania danych z pliku. """ def load_from_file(self, file_name: str): file = open(file_name, "r") self.tasks_nb = int(next(file)) for id in range(0, self.tasks_nb): task = Task() row = next(file).split() task.time = (int(row[0])) task.penalty = (int(row[1])) task.deadline = (int(row[2])) task.id = id self.tasks.append(task) ################################################################################### """ Metoda solve uruchamia solver. """ def solve(self): print("\nSolver włączony") self.wt_solver.parameters.max_time_in_seconds = 8 self.wt_solver.Solve(self.wt_model) print("Solver zakończony\n") ################################################################################### """ Metoda run jest podstawową metodą definiowania problemu. """ def run(self, file_name): self.load_from_file(file_name) time_sum = 0 for task_nbr in range(0, self.tasks_nb): time_sum = time_sum + self.tasks[task_nbr].time late_sum = 0 for task_nbr in range(self.tasks_nb): late_sum += self.tasks[task_nbr].penalty * self.tasks[ task_nbr].deadline objective_min = 0 objective_max = late_sum + 1 variable_max_value = 1 + time_sum variable_min_value = 0 starts = [] finishes = [] intervals = [] lates = [] objective = self.wt_model.NewIntVar(objective_min, objective_max, "WiTi") for task_nbr in range(self.tasks_nb): nbr = str(task_nbr) start = self.wt_model.NewIntVar(variable_min_value, variable_max_value, "start" + nbr) finish = self.wt_model.NewIntVar(variable_min_value, variable_max_value, "finish" + nbr) interval = self.wt_model.NewIntervalVar(start, self.tasks[task_nbr].time, finish, "interval" + nbr) late = self.wt_model.NewIntVar(objective_min, objective_max, "late" + nbr) starts.append(start) finishes.append(finish) intervals.append(interval) lates.append(late) self.wt_model.AddNoOverlap(intervals) for task_nbr in range(self.tasks_nb): self.wt_model.Add(lates[task_nbr] >= 0) self.wt_model.Add( lates[task_nbr] >= (finishes[task_nbr] - self.tasks[task_nbr].deadline) * self.tasks[task_nbr].penalty) max_t = sum(lates) self.wt_model.Add(objective >= max_t) self.wt_model.Minimize(objective) self.solve() output_tasks_order = [] for task_nbr in range(self.tasks_nb): output_tasks_order.append( (task_nbr, self.wt_solver.Value(starts[task_nbr]))) output_tasks_order.sort(key=lambda x: x[1]) output_tasks_order = [x[0] for x in output_tasks_order] print("Suma: " + str(int(self.wt_solver.ObjectiveValue()))) print("Kolejność zadań: " + str(output_tasks_order))
class JSProblem: js_model = CpModel() js_solver = CpSolver() tasks_nb = 0 machines_nb = 0 operations_nb = 0 """ Metoda load_from_file służy do załadowania danych z pliku. """ def load_from_file(self, file_name: str): file = open(file_name, "r") self.tasks_nb, self.machines_nb, self.operations_nb = [ int(x) for x in next(file).split() ] jobshop_data = [] for i in range(0, self.tasks_nb): row = next(file).split() operation_in_task = int(row[0]) single_job_data = [] for i in range(1, operation_in_task * 2, 2): m = int(row[i]) p = int(row[i + 1]) single_job_data.append((m, p)) jobshop_data.append(single_job_data) file.close() return jobshop_data ################################################################################### """ Metoda solve uruchamia solver. """ def solve(self): print("\nSolver włączony") self.js_solver.parameters.max_time_in_seconds = 8 self.js_solver.Solve(self.js_model) print("Solver zakończony\n") ################################################################################### """ Metoda print_result zajmuje się wypisaniem wyników działania solvera. """ def print_result(self, jobshop_matrix, all_tasks): assigned_task_type = cl.namedtuple('assigned_task_type', 'start job index') assigned_jobs = cl.defaultdict(list) for job_id, job in enumerate(jobshop_matrix): for task_id, task in enumerate(job): machine = task[0] assigned_jobs[machine].append( assigned_task_type( start=self.js_solver.Value(all_tasks[job_id, task_id].start), job=job_id, index=task_id, )) print("Cmax wynosi: " + str(int(self.js_solver.ObjectiveValue()))) for machine in range(1, self.machines_nb + 1): assigned_jobs[machine].sort() line_to_print = "Maszyna " + str(machine) + ': ' for assigned_task in assigned_jobs[machine]: name = assigned_task.job * self.machines_nb + assigned_task.index + 1 line_to_print += str(name) + " " print(line_to_print) ################################################################################### """ Metoda run jest podstawową metodą definiowania problemu. """ def run(self, filename): jobshop_data = self.load_from_file(filename) task_type = cl.namedtuple('task_type', 'start end interval') all_tasks = {} machine_to_intervals = cl.defaultdict(list) worst_cmax = sum(task[1] for job in jobshop_data for task in job) for job_id, job in enumerate(jobshop_data): for task_id, task in enumerate(job): machine = task[0] duration = task[1] start = self.js_model.NewIntVar(0, worst_cmax, 'start') finish = self.js_model.NewIntVar(0, worst_cmax, 'finish') interval_var = self.js_model.NewIntervalVar( start, duration, finish, 'interval') all_tasks[job_id, task_id] = task_type(start=start, end=finish, interval=interval_var) machine_to_intervals[machine].append(interval_var) for machine in range(1, self.machines_nb + 1): self.js_model.AddNoOverlap(machine_to_intervals[machine]) for job_id, job in enumerate(jobshop_data): for task_id in range(len(job) - 1): self.js_model.Add( all_tasks[job_id, task_id + 1].start >= all_tasks[job_id, task_id].end) cmax = self.js_model.NewIntVar(0, worst_cmax, 'cmax') self.js_model.AddMaxEquality(cmax, [ all_tasks[job_id, len(job) - 1].end for job_id, job in enumerate(jobshop_data) ]) self.js_model.Minimize(cmax) self.solve() self.print_result(jobshop_data, all_tasks)
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 solve_problem(model: CpModel, solver: CpSolver) -> Any: return solver.Solve(model)
def hint_solution(model: cp_model.CpModel, solver: cp_model.CpSolver) -> None: """Hint all the variables of a model with its solution.""" model.Proto().solution_hint.Clear() variables = range(len(model.Proto().variables)) model.Proto().solution_hint.vars.extend(variables) model.Proto().solution_hint.values.extend(solver.ResponseProto().solution)
class _Solver: """Generic solver class.""" def __init__(self): self.model = CpModel() self.solver = CpSolver() # Ideal number of workers is 8, see https://or.stackexchange.com/a/4126 self.solver.parameters.num_search_workers = 8 self._known_names = set() self._vars_bool = {} self._vars_int = {} self._vars_weighted = [] self._vars_weighted_cost = [] self.printer = ObjectiveSolutionPrinter() def create_variable_bool(self, name: str = None) -> IntVar: """Create a boolean variable. :param name: The variable human readable name. :return: The new variable """ assert name not in self._vars_bool assert name not in self._known_names var = self.model.NewBoolVar(name) self._vars_bool[name] = var return var def create_variable_int( self, key: str, minimum: int, maximum: int, name: str = None ) -> IntVar: """Create an integer variable. :param key: An hashable value to use as an identifier. :param minimum: The variable domain minimum value. :param maximum: The variable domain maximum value. :param name: The variable human readable name. :return: The new variable """ name = name or "_".join(key) assert key not in self._vars_int assert name not in self._known_names var = self.model.NewIntVar(minimum, maximum, name) self._vars_int[key] = var return var def get_variable_bool(self, key: Hashable) -> IntVar: """Get an already defined boolean variable. :param key: A hashable key :return: A variable :raises KeyError: If no variable exist for the provided key. """ return self._vars_bool[key] def add_hard_constraint(self, expr: BoundedLinearExpression) -> Constraint: """Add a "hard" constraint. T he solve will fail if even once hard constraint fail to be satisfied. :param expr: The constraint expression :return: A constraint """ return self.model.Add(expr) def set_variable_bool_score(self, variable: Constraint, score: int) -> None: """Set the cost for a variable. :param variable: A variable :param score: The cost if the variant is ON """ self._vars_weighted.append(variable) self._vars_weighted_cost.append(score) def create_soft_constraint_bool(self, name: str, score: int) -> Constraint: """Add a "soft" boolean variable with a score. The solver will try to maximize it's score. :param str name: The variable name :param int score: The variable score :return: A constraint """ assert name not in self._known_names var = self.model.NewBoolVar(name) self.set_variable_bool_score(var, score) return var def create_soft_constraint_int( self, name: str, minimum: int, maximum: int, score: int ) -> IntVar: """Add a "soft" integer variable with a score. The solver will try to maximum it's score. :param name: The variable name :param minimum: The variable domain minimum value :param maximum: The variable domain maximum value :param score: The variable score :return: A constraint """ assert name not in self._known_names var = self.model.NewIntVar(minimum, maximum, name) self._vars_weighted.append(var) self._vars_weighted_cost.append(score) return var def iter_variables_and_cost(self) -> Generator[Tuple[IntVar, int], None, None]: """Yield all variables and their score multipliers.""" for variable, score in zip(self._vars_weighted, self._vars_weighted_cost): yield variable, score def solve(self): """Solve using provided constraints.""" self.model.Maximize( sum( var * cost for var, cost in zip( itertools.chain( self._vars_weighted, self._vars_weighted, ), itertools.chain( self._vars_weighted_cost, self._vars_weighted_cost, ), ) ) ) status = self.solver.SolveWithSolutionCallback(self.model, self.printer) if status not in (OPTIMAL, FEASIBLE): raise RuntimeError("No solution found! Status is %s" % status) return status
def get_solved_values(solver: CpSolver, letters: List) -> List[int]: return list(solver.Value(letter) for letter in letters)
def optimize( self, input_params: InputParameters, timing_object: TimingData) -> Tuple[Testcase, EOptimizationStatus]: # Solve model_Pint. solver = CpSolver() solver.parameters.max_time_in_seconds = input_params.timeouts.timeout_routing print_model_stats(self.model.ModelStats()) t = Timer() with t: for f_int in range(self.max_stream_int): self.model.AddDecisionStrategy( list(self.x[f_int]), cp_model.CHOOSE_LOWEST_MIN, cp_model.SELECT_MIN_VALUE, ) self.model.AddDecisionStrategy( list(self.x_v_has_successor[f_int]), cp_model.CHOOSE_LOWEST_MIN, cp_model.SELECT_MIN_VALUE, ) self.model.AddDecisionStrategy( list(self.y[f_int]), cp_model.CHOOSE_LOWEST_MIN, cp_model.SELECT_MIN_VALUE, ) solver_status = solver.Solve(self.model) # print(solver.ResponseStats()) timing_object.time_optimizing_routing = t.elapsed_time 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 # Output if (status == EOptimizationStatus.FEASIBLE or status == EOptimizationStatus.OPTIMAL): x_res, costs, route_lens, overlap_amounts, overlap_links = routing_model_results.generate_result_structures( self, solver) for f in self.tc.F_routed.values(): mt = route(f) mt.init_from_x_res_vector(x_res[f.id]) self.tc.add_to_datastructures(mt) r_info = route_info(mt, costs[f.id], route_lens[f.id], overlap_amounts[f.id], overlap_links[f.id]) self.tc.add_to_datastructures(r_info) else: raise ValueError( "CPSolver in RoutingModel returned invalid status for routing model:" + str(status)) return self.tc, status
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
class NQueensSolver(object): def __init__(self, num_queens: int, printer: bool): # Initialize model and solver self._cp_model = CpModel() self._cp_solver = CpSolver() self._num_queens = num_queens self._indices = range(num_queens) # Initialize the board self._board = self._initialize_board() # Add constraint for exactly 1 queen in each row, col self._constrain_rows_and_columns() # Add constraint for at most 1 queen in each diagonal self.constrain_diagonal(self.backwards_diagonal_func()) self.constrain_diagonal(self.forwards_diagonal_func()) # Add constraint for exactly N queens on board self._constrain_num_queens() # initialize solution printer self._solution_printer = NQueensPrinter(self._board, printer) def solve(self): """ This function runs the SAT solver on the generated constraint model and prints the number of solutions """ self._cp_solver.SearchForAllSolutions(self._cp_model, self._solution_printer) print('Total Solutions: %i' % self._solution_printer.count()) def _initialize_board(self) -> List[List[IntVar]]: """ This function initalized a NxN board of IntVars to be constrained This can be thought of as the board where a 0 represents an empty space and a 1 represents a queen. Returns: A 2d list of size NxN containing IntVars """ # Add NxN new spots to the model return [[self._add_new_spot(i, j) for i in self._indices] for j in self._indices] def _add_new_spot(self, i: int, j: int) -> IntVar: """ This function Generates the IntVar to be added to the board Args: i: the row index of the IntVar j: the col index of the IntVar Returns: A newly generated IntVar constrained to the values 0..1 """ # Adds a new boolean variable ot the solver, with the name 'posx,y' return self._cp_model.NewBoolVar(f"pos{i},{j}") def _constrain_rows_and_columns(self): """ This function generates the row and column constraints for the board. We ensure that there is 1 and only 1 queen in a given row and column Returns: None """ for i in self._indices: # AddBoolXOr ensures exactly one queen in each row & each col self._cp_model.AddBoolXOr(self._board[i]) # lets break this down part by part: # *a is destructuring the list into separate arguments to zip # zip takes each element at the same index and assembles them into their own list # so zip([1,2,3], [3,2,1]) = [(1,3), (2,2) (1,3)] # In our case, this results in getting the columns of our board! self._cp_model.AddBoolXOr(list(zip(*self._board))[i]) def _constrain_num_queens(self): """ This function adds the constraint that we must have N queens on the board. No more, no less. Returns: None """ queens = None for i in self._indices: for j in self._indices: queens = self.add(queens, self._board[i][j]) self._cp_model.Add(queens == self._num_queens) def constrain_diagonal(self, function: Callable[[List[None], int, List[IntVar]], List[List[IntVar]]]): """ This function ensures that there are either 0 or 1 queens in a diagonal. Args: function: A function which returns a list of lists of IntVars. Each sublist represents the IntVars on a diagonal. Returns: None """ b = [None] * (self._num_queens - 1) board = [function(b, i, r) for i, r in enumerate(self._board)] # Essentially, this works by padding rows ascending or descending depending if we want forwards or backwards diags # We then take the columns of this and ignore the null padding # 1 2 3 |X|X|1|2|3| | | |1|2|3| # 4 5 6 => |X|4|5|6|X| => | |4|5|6| | => [[7],[4,8],[1,5,9],[2,6],[3]] # 7 8 9 |7|8|9|X|X| |7|8|9| | | [ self._cp_model.Add(self.sum_queens(diag) <= 1) for diag in list(zip(*board)) ] def sum_queens(self, diag: List[IntVar]) -> _SumArray: """ This function generates a sum of the IntVars on a diagonal. Args: diag: A list of IntVars to create the sum constraint on Returns: a SumArray to constrain on a particular diagonal """ fd_sum = None for item in diag: if item is not None: fd_sum = self.add(fd_sum, item) return fd_sum @staticmethod def add(l: Union[_SumArray, None], item: IntVar) -> Union[_SumArray, IntVar]: """ This function either adds an item (IntVar) into a SumArray The union type allows us to handle the case of the sumarray being null on the first iteration Args: l: A union type representing either a SumArray or None item: The constraint to add to the SumArray Returns: Either an IntVar or a SumArray """ return l + item if l is not None else item @staticmethod def backwards_diagonal_func( ) -> Callable[[List[Any], int, List[Any]], List[List[Any]]]: """ This function returns a lambda fn that will get the backwards diagonals from a list Returns: The aformentioned lambda fn (go functions as values!!) """ return lambda b, i, r: (b[i:] + r + b[:i]) @staticmethod def forwards_diagonal_func( ) -> Callable[[List[Any], int, List[Any]], List[List[Any]]]: """ This function returns a lambda fn that will get the forwards diagonals from a list Returns: The aforementioned Lambda fn """ return lambda b, i, r: (b[:i] + r + b[i:])