Example #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
Example #2
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))
            )
        )
Example #3
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 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