Exemple #1
0
 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
Exemple #2
0
    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
Exemple #3
0
 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()
Exemple #4
0
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
Exemple #5
0
    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)
Exemple #6
0
    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}")
Exemple #8
0
    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))
            )
        )
Exemple #9
0
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))
Exemple #10
0
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)
Exemple #13
0
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)
Exemple #14
0
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)
Exemple #16
0
    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
Exemple #18
0
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:])