Ejemplo n.º 1
0
    def to_tsplib95(self):
        arcs = TupList(self.data["arcs"])
        nodes = (arcs.take("n1") + arcs.take("n2")).unique()
        pos = {k: v for v, k in enumerate(nodes)}
        arc_dict = arcs.to_dict(result_col="w",
                                indices=["n1", "n2"],
                                is_list=False).to_dictdict()
        arc_weights = [[]] * len(nodes)
        for n1, n2dict in arc_dict.items():
            n1list = arc_weights[pos[n1]] = [0] * len(n2dict)
            for n2, w in n2dict.items():
                n1list[pos[n2]] = w

        if len(nodes)**2 == len(arcs):
            edge_weight_format = "FULL_MATRIX"
        elif abs(len(nodes)**2 - len(arcs) * 2) <= 2:
            edge_weight_format = "LOWER_DIAG_ROW"
        else:
            # TODO: can there another possibility?
            edge_weight_format = "LOWER_DIAG_ROW"
        dict_data = dict(
            name="TSP",
            type="TSP",
            comment="",
            dimension=len(nodes),
            edge_weight_type="EXPLICIT",
            edge_weight_format=edge_weight_format,
            edge_weights=arc_weights,
        )
        return tsp.models.StandardProblem(**dict_data)
Ejemplo n.º 2
0
    def create_constraints(self, model, used_routes, artificial_variables):
        # Indices
        ind_td_routes = TupList(self.get_td_routes(used_routes))
        ind_td_routes_r = ind_td_routes.to_dict(result_col=[1, 2])
        ind_customers_hours = self.get_customers_hours()
        _sum_c7_c12_domain = {
            i: self.get_sum_c7_c12_domain(used_routes, i)
            for i in self.instance.get_id_customers()
        }

        # Constraints (1), (2) - Minimize the total cost
        costs = ind_td_routes.to_dict(None).vapply(lambda v: self.cost(*v))
        objective = pl.lpSum(self.route_var * costs)
        if artificial_variables:
            objective += pl.lpSum(2000 *
                                  self.artificial_binary_var.values_tl())
        model += objective, "Objective"
        self.print_in_console("Added (Objective) - w/o art. vars.")

        # Constraints : (3) - A shift is only realized by one driver with one driver
        for r, tr_dr in ind_td_routes_r.items():
            model += (
                pl.lpSum(self.route_var[r, tr, dr] for tr, dr in tr_dr) <= 1,
                f"C3_r{r}",
            )
        self.print_in_console("Added (3)")

        # Constraints : (7) - Conservation of the inventory
        for (i, h) in ind_customers_hours:
            artificial_var = 0
            if artificial_variables:
                artificial_var = self.artificial_quantities_var[i, h]
            model += (-self.inventory_var[i, h] +
                      self.inventory_var[i, h - 1] -
                      pl.lpSum(self.quantity_var[i, r, tr, k] *
                               self.k_visit_hour[i, h, r, k]
                               for (r, tr, k) in _sum_c7_c12_domain[i]) -
                      artificial_var - self.instance.get_customer_property(
                          i, "Forecast")[h] == 0), f"C7_i{i}_h{h}"

        for i in self.all_customers:
            initial_tank = self.instance.get_customer_property(
                i, "InitialTankQuantity")
            model += self.inventory_var[i, -1] == initial_tank, f"C7_i{i}_h-1"
        self.print_in_console("Added (7)")

        # Constraints: (A2) - The quantity delivered in an artificial delivery respects quantities constraints
        if artificial_variables:
            for (i, h) in ind_customers_hours:
                model += (
                    self.artificial_quantities_var[i, h] +
                    self.artificial_binary_var[i, h] * min(
                        self.instance.get_customer_property(i, "Capacity"),
                        self.routes_generator.max_trailer_capacities,
                    ) >= 0), f"CA2_i{i}_h{h}"
            self.print_in_console(f"Added (A2)")

        # Constraints : (9) - Conservation of the trailers' inventories
        _sum_c9_domain = self.get_sum_c9_domain(used_routes)
        for (tr, h) in self.get_c4_c9_domain():
            model += (pl.lpSum(self.quantity_var[i, r, tr, k] *
                               self.k_visit_hour[(i, h, r, k)]
                               for (r, i, k) in _sum_c9_domain) +
                      self.trailer_quantity_var[tr, h - 1] ==
                      self.trailer_quantity_var[tr, h]), f"C9_tr{tr}_h{h}"

        for tr in self.instance.get_id_trailers():
            initial_quantity = self.instance.get_trailer_property(
                tr, "InitialQuantity")
            model += (
                self.trailer_quantity_var[tr, -1] == initial_quantity,
                f"C9_tr{tr}_h-1",
            )
        self.print_in_console("Added (9)")

        # Constraints: (15) - Conservation of the total inventory between the beginning of the time horizon and the end
        model += (pl.lpSum(
            self.coef_inventory_conservation * self.inventory_var[i, -1] -
            self.inventory_var[i, self.horizon - 1]
            for i in self.instance.get_id_customers()) <= 0), f"C15"
        self.print_in_console("Added (15)")

        # Constraints: (10) - Quantities delivered don't exceed trailer capacity
        _drivers = self.instance.get_id_drivers()
        for (r, i, tr, k) in self.get_c10_domain(used_routes):
            _capacity = self.instance.get_customer_property(i, "Capacity")
            model += (
                pl.lpSum(self.route_var[r, tr, dr] * _capacity
                         for dr in _drivers) >=
                -self.quantity_var[i, r, tr, k],
                f"C10_i{i}_r{r}_tr{tr}_k{k}",
            )
        self.print_in_console("Added (10)")

        # Constraints: (11), (12)
        for (r, route, i, tr, k) in self.get_c11_c12_domain(used_routes):
            # Constraint: (11) - Quantities delivered don't exceed the quantity in the trailer
            visited = lambda j: route.visited[j][0]
            q_tup = lambda j, kp: (visited(j), r, tr, kp)
            hour_tup = lambda j, kp: (
                visited(j),
                self.hour_of_visit[i, r, k],
                r,
                kp,
            )
            visit_tup = lambda j, kp: (r, visited(j), kp, i, k)

            model += (
                pl.lpSum((self.quantity_var[q_tup(j, kp)] *
                          self.k_visit_hour[hour_tup(j, kp)] *
                          self.visit_before_on_route(*visit_tup(j, kp)))
                         for (j, kp) in self.get_sum_c11_c14_domain(i, r, k)) +
                self.quantity_var[i, r, tr, k] + self.trailer_quantity_var[
                    (tr, self.hour_of_visit[i, r, k] - 1)] >=
                0), f"C11_i{i}_r{r}_tr{tr}_k{k}"

            # Constraint: (12) - Quantities delivered don't exceed available space in customer tank
            model += (pl.lpSum(
                (self.quantity_var[i, rp, trp, kp] *
                 self.k_visit_hour[i, self.hour_of_visit[i, r, k], rp, kp] *
                 self.visit_before(i, r, k, rp, kp), )
                for (rp, trp, kp) in _sum_c7_c12_domain[i]
                if r != rp and tr != trp) -
                      self.inventory_var[i, self.hour_of_visit[i, r, k] - 1] +
                      self.quantity_var[i, r, tr, k] +
                      self.instance.get_customer_property(i, "Capacity") >=
                      0), f"C12_i{i}_r{r}_tr{tr}_k{k}"
        self.print_in_console("Added (11), (12)")

        # Constraints: (13) - Quantities loaded at a source don't exceed trailer capacity
        _drivers = self.instance.get_id_drivers()
        for (r, i, tr, k) in self.get_c13_domain(used_routes):
            _capacity = self.instance.get_trailer_property(tr, "Capacity")
            model += (self.quantity_var[i, r, tr, k] <=
                      pl.lpSum(self.route_var[r, tr, dr] for dr in _drivers) *
                      _capacity), f"C13_i{i}_r{r}_tr{tr}_k{k}"
        self.print_in_console("Added (13)")

        # Constraints: (14) - Quantities loaded at a source don't exceed free space in the trailer
        for (r, route, i, tr, k) in self.get_c14_domain(used_routes):
            visited = lambda j: route.visited[j][0]
            q_tup = lambda j, kp: (visited(j), r, tr, kp)
            hour_tup = lambda j, kp: (
                visited(j),
                self.hour_of_visit[(i, r, k)],
                r,
                kp,
            )
            visit_tup = lambda j, kp: (r, visited(j), kp, i, k)
            model += (
                pl.lpSum((self.quantity_var[q_tup(j, kp)] *
                          self.k_visit_hour[hour_tup(j, kp)] *
                          self.visit_before_on_route(*visit_tup(j, kp)), )
                         for (j, kp) in self.get_sum_c11_c14_domain(i, r, k)) +
                self.quantity_var[i, r, tr, k] + self.trailer_quantity_var[
                    (tr, self.hour_of_visit[(i, r, k)] - 1)] <=
                self.instance.get_trailer_property(
                    tr, "Capacity")), f"C14_i{i}_r{r}_tr{tr}_k{k}"
        self.print_in_console("Added (14)")

        # Constraints (4), (5) - Two shifts with same trailer can't happen at the same time
        #   and two shifts realized by the same driver must leave time for the driver to rest between both
        _sum_c4_domain = self.get_sum_c4_domain(used_routes)
        _sum_c5_domain = self.get_sum_c5_domain(used_routes)
        for (tr, h) in self.get_c4_c9_domain():
            model += (
                pl.lpSum(self.route_var[r, tr, dr]
                         for r, route, dr in _sum_c4_domain
                         if self.runs_at_hour(r, h)) <= 1,
                f"C4_tr{tr}_h{h}",
            )
        self.print_in_console("Added (4)")

        for (dr, h) in self.get_c5_domain():
            model += (
                pl.lpSum(self.route_var[r, tr, dr]
                         for r, route, tr in _sum_c5_domain
                         if self.blocks_driver_at_hour(r, dr, h)) <= 1,
                f"C5_dr{dr}_h{h}",
            )
        self.print_in_console("Added (5)")

        return model
Ejemplo n.º 3
0
class MipModel(Experiment):
    def __init__(self, instance, solution=None):
        super().__init__(instance, solution)

        # Sets and parameters
        self.employee_ts_availability = TupList()
        self.ts_employees = SuperDict()
        self.ts_managers = SuperDict()
        self.ts_open = TupList()
        self.max_working_ts_week = SuperDict()
        self.workable_ts_week = SuperDict()
        self.max_working_ts_day = SuperDict()
        self.min_working_ts_day = SuperDict()
        self.workable_ts_day = SuperDict()
        self.ts_ts_employee = SuperDict()
        self.max_working_days = SuperDict()
        self.managers = TupList()
        self.incompatible_ts_employee = TupList()
        self.first_ts_day_employee = SuperDict()
        self.demand = SuperDict()
        self.ts_demand_employee_skill = SuperDict()

        # Variables
        self.works = SuperDict()
        self.starts = SuperDict()

        self.initialize()

    def solve(self, options: dict) -> dict:

        model = pl.LpProblem("rostering", pl.LpMaximize)
        # Variables:
        self.create_variables()
        # Constraints:
        model = self.create_constraints(model)
        # print(model)

        # Solver and solve
        mat_solver = pl.PULP_CBC_CMD(
            gapRel=0.001,
            timeLimit=options.get("timeLimit", 240),
            msg=options.get("msg", False),
        )
        status = model.solve(mat_solver)

        # Check status
        if model.sol_status not in [pl.LpSolutionIntegerFeasible, pl.LpSolutionOptimal]:
            return dict(status=status, status_sol=SOLUTION_STATUS_INFEASIBLE)

        work_assignments = (
            self.works.vfilter(lambda v: pl.value(v))
            .keys_tl()
            .vapply(lambda v: dict(id_employee=v[1], time_slot=v[0]))
        )

        self.solution = Solution.from_dict(SuperDict(works=work_assignments))

        return dict(status=status, status_sol=SOLUTION_STATUS_FEASIBLE)

    def initialize(self):
        self.managers = self.instance.get_employees_managers()
        self.employee_ts_availability = self.instance.get_employees_ts_availability()
        self.ts_employees = self.employee_ts_availability.to_dict(1)
        self.ts_managers = self.ts_employees.vapply(
            lambda v: [e for e in v if e in self.managers]
        )

        self.ts_open = self.ts_employees.keys_tl()

        self.max_working_ts_week = self.instance.get_max_working_slots_week()
        self.workable_ts_week = self.instance.get_employees_time_slots_week()
        self.max_working_ts_day = self.instance.get_max_working_slots_day()
        self.min_working_ts_day = self.instance.get_min_working_slots_day()
        self.workable_ts_day = self.instance.get_employees_time_slots_day()
        self.ts_ts_employee = self.instance.get_consecutive_time_slots_employee()
        self.incompatible_ts_employee = self.instance.get_incompatible_slots_employee()
        self.first_ts_day_employee = self.instance.get_first_time_slot_day_employee()
        self.max_working_days = self.instance.get_max_working_days()

        self.demand = self.instance.get_demand()
        self.ts_demand_employee_skill = self.instance.get_ts_demand_employees_skill()

    def create_variables(self):

        self.works = pl.LpVariable.dicts(
            "works",
            self.employee_ts_availability,
            lowBound=0,
            upBound=1,
            cat=pl.LpBinary,
        )

        self.works = SuperDict(self.works)

        self.starts = pl.LpVariable.dicts(
            "starts",
            self.employee_ts_availability,
            lowBound=0,
            upBound=1,
            cat=pl.LpBinary,
        )

        self.starts = SuperDict(self.starts)

    def create_constraints(self, model):
        # RQ00: objective function - minimize working hours
        model += pl.lpSum(
            pl.lpSum(self.works[ts, e] for e in self.ts_employees[ts]) * self.demand[ts]
            for ts in self.ts_open
        )

        # RQ01: at least one employee at all times
        for ts, _employees in self.ts_employees.items():
            model += pl.lpSum(self.works[ts, e] for e in _employees) >= 1

        # RQ02: employees work their weekly hours
        for (w, e), max_slots in self.max_working_ts_week.items():
            model += (
                pl.lpSum(self.works[ts, e] for ts in self.workable_ts_week[w, e])
                == max_slots
            )

        # RQ03: employees can not exceed their daily hours
        for (d, e), slots in self.workable_ts_day.items():
            model += (
                pl.lpSum(self.works[ts, e] for ts in slots)
                <= self.max_working_ts_day[d, e]
            )

        # RQ04A: starts if does not work in one ts but in the next it does
        for (ts, ts2, e) in self.ts_ts_employee:
            model += self.works[ts, e] >= self.works[ts2, e] - self.starts[ts2, e]

        # RQ04B: starts on first time slot
        for (d, e), ts in self.first_ts_day_employee.items():
            model += self.works[ts, e] == self.starts[ts, e]

        # RQ04C: only one start per day
        for (d, e), slots in self.workable_ts_day.items():
            model += pl.lpSum(self.starts[ts, e] for ts in slots) <= 1

        # RQ05: max days worked per week
        for (w, e), slots in self.workable_ts_week.items():
            model += (
                pl.lpSum(self.starts[ts, e] for ts in slots)
                <= self.max_working_days[w, e]
            )

        # RQ06: employees at least work the minimum hours
        for (d, e), slots in self.workable_ts_day.items():
            model += pl.lpSum(
                self.works[ts, e] for ts in slots
            ) >= self.min_working_ts_day[d, e] * pl.lpSum(
                self.starts[ts, e] for ts in slots
            )

        # RQ07: employees at least have to rest an amount of hours between working days.
        for (ts, ts2, e) in self.incompatible_ts_employee:
            model += self.works[ts, e] + self.works[ts2, e] <= 1

        # RQ08: a manager has to be working at all times
        for ts, _employees in self.ts_managers.items():
            model += pl.lpSum(self.works[ts, e] for e in _employees) >= 1

        # RQ09: The demand for each skill should be covered
        for ts, id_skill, skill_demand, _employees in self.ts_demand_employee_skill:
            model += pl.lpSum(self.works[ts, e] for e in _employees) >= skill_demand

        return model