Example #1
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
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