예제 #1
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
예제 #2
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
예제 #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
예제 #4
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 solve_problem(model: CpModel, solver: CpSolver) -> Any:
    return solver.Solve(model)
예제 #6
0
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
예제 #7
0
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