Exemplo n.º 1
0
    def from_file(cls, path):
        data = SuperDict(suppliers=dict(sheet_name="suppliers_L1",
                                        index_col=[0, 1, 2]),
                         products=dict(sheet_name="products",
                                       index_col=[0, 1, 2]),
                         clients=dict(sheet_name="clients", index_col=[0, 1]),
                         warehouses=dict(sheet_name="warehouses",
                                         index_col=[0, 1, 2, 3]),
                         distances=dict(sheet_name="distances",
                                        index_col=[0, 1, 2]),
                         restricted_flows=dict(sheet_name="not_allowed_flows",
                                               index_col=[0, 1]))

        def read_table(**kwargs):
            return pd.read_excel(filename=path, header=0,
                                 **kwargs).index.values.tolist()

        return cls(data.vapply(lambda v: read_table(**v)))
Exemplo n.º 2
0
class MIPModel(Experiment):
    def __init__(self, instance, solution=None):
        super().__init__(instance, solution)
        self.log = ""
        self.solver = "MIP Model"
        self.routes_generator = RoutesGenerator(self.instance)
        self.range_hours = list(range(self.horizon))
        self.routes = dict()
        self.unused_routes = dict()
        self.value_greedy = None
        self.interval_routes = 6
        self.nb_routes = 1000
        self.start_time = datetime.now()
        self.start_time_string = datetime.now().strftime("%d.%m-%Hh%M")
        self.print_log = False
        self.save_results = False
        self.artificial_quantities = dict()
        self.limit_artificial_round = 0
        self.last_solution_round = -1
        self.solution_greedy = None
        self.locations_in = dict()
        self.unique_locations_in = dict()
        self.hour_of_visit = dict()
        self.k_visit_hour = dict()
        self.nb_visits = dict()
        self.coef_inventory_conservation = 0.8
        self.time_limit = 100000

        # Variables
        self.route_var = SuperDict()
        self.artificial_quantities_var = SuperDict()
        self.artificial_binary_var = SuperDict()
        self.inventory_var = SuperDict()
        self.quantity_var = SuperDict()
        self.trailer_quantity_var = SuperDict()

    @staticmethod
    def get_solver(config):
        solver_name = config.pop("solver")
        if "." in solver_name:
            return solver_name.split(".")[1]
        return "CPLEX_PY"

    def solve(self, config=None):
        self.start_time = datetime.now()
        self.start_time_string = datetime.now().strftime("%d.%m-%Hh%M")

        if config is None:
            config = dict()
        config = dict(config)

        self.time_limit = config.get("timeLimit", 100000)
        self.coef_inventory_conservation = config.get(
            "inventoryConservation", self.coef_inventory_conservation)

        self.print_in_console("Started at: ", self.start_time_string)

        self.nb_routes = config.get("nb_routes_per_run", self.nb_routes)

        self.routes = self.generate_initial_routes()
        previous_value = self.value_greedy

        self.unused_routes = pickle.loads(pickle.dumps(self.routes, -1))
        used_routes = dict()
        current_round = 0

        self.print_in_console(
            "=================== ROUND 0 ========================")
        self.print_in_console("Initial empty solving at: ",
                              datetime.now().strftime("%H:%M:%S"))
        solver_name = self.get_solver(config)
        config_first = dict(
            solver=solver_name,
            gapRel=0.1,
            timeLimit=min(200.0, self._get_remaining_time()),
            msg=self.print_log,
        )

        def config_iteration(self, warm_start):
            return dict(
                solver=solver_name,
                gapRel=0.05,
                timeLimit=min(100.0, self._get_remaining_time()),
                msg=self.print_log,
                warmStart=warm_start,
            )

        solver = pl.getSolver(**config_first)
        used_routes, previous_value = self.solve_one_iteration(
            solver, used_routes, previous_value, current_round)
        current_round += 1
        while len(self.unused_routes) != 0 and self._get_remaining_time() > 0:
            self.print_in_console(
                f"=================== ROUND {current_round} ========================"
            )
            solver = pl.getSolver(**config_iteration(self, current_round != 1))

            used_routes, previous_value = self.solve_one_iteration(
                solver, used_routes, previous_value, current_round)
            current_round += 1

        self.set_final_id_shifts()
        self.post_process()

        self.print_in_console(used_routes)

        if self.save_results:
            with open(
                    f"res/solution-schema-{self.start_time_string}-final.json",
                    "w") as fd:
                json.dump(self.solution.to_dict(), fd)

        return 1

    def solve_one_iteration(self, solver, used_routes, previous_value,
                            current_round):
        if 0 < current_round <= self.limit_artificial_round + 1:
            self.generate_new_routes()
            self.artificial_quantities = dict()

        old_used_routes = pickle.loads(pickle.dumps(used_routes, -1))
        previous_routes_infos = [(shift["id_shift"], shift["trailer"],
                                  shift["driver"])
                                 for shift in self.solution.get_all_shifts()]

        if current_round > 0:
            selected_routes = self.select_routes(self.nb_routes)
            used_routes = SuperDict({**used_routes, **selected_routes})
            self.initialize_parameters(used_routes)

        model = self.new_model(
            used_routes,
            previous_routes_infos,
            current_round <= self.limit_artificial_round,
        )
        status = model.solve(solver=solver)

        if status == 1:
            self.to_solution(model, used_routes, current_round)
            if current_round > self.limit_artificial_round:
                self.check_and_save(current_round)

        if status != 1 or current_round == 0:
            used_routes = old_used_routes
            return used_routes, None

        if not (previous_value is None
                or pl.value(model.objective) < previous_value or
                ((current_round > self.limit_artificial_round) and
                 (self.last_solution_round <= self.limit_artificial_round))):
            return old_used_routes, previous_value

        keep = (self.route_var.vfilter(lambda v: pl.value(v) > 0.5).keys_tl().
                take(0).to_dict(None).vapply(lambda v: True))

        used_routes = {
            r: route
            for r, route in used_routes.items() if keep.get(r, False)
        }

        if current_round > self.limit_artificial_round:
            previous_value = pl.value(model.objective)
        self.last_solution_round = current_round
        return used_routes, previous_value

    def _get_remaining_time(self) -> float:
        return self.time_limit - (datetime.now() -
                                  self.start_time).total_seconds()

    def generate_initial_routes(self):
        """
        Generates shifts from initial methods
        The generated shifts already have a departure time and respect the duration constraint
        :return: A dictionary whose keys are the indices of the shifts and whose values are instances of RouteLabel
        """
        # 'shortestRoutes', 'FromForecast', 'RST'
        (
            routes,
            nb_greedy,
            self.value_greedy,
            self.solution_greedy,
        ) = self.routes_generator.generate_initial_routes(
            methods=["FromClusters"], unique=True, nb_random=0)
        self.print_in_console("Value greedy: ", self.value_greedy)

        partial_interval_routes = self.interval_routes
        while nb_greedy * (self.horizon // partial_interval_routes) > 400:
            partial_interval_routes = 2 * partial_interval_routes

        routes = [
            route.copy().get_label_with_start(start) for offset in range(
                0, partial_interval_routes, self.interval_routes)
            for route in routes
            for start in range(offset, self.horizon, partial_interval_routes)
            if start + MIPModel.duration_route(route) < self.horizon
        ]

        self.nb_routes = max(
            self.nb_routes,
            nb_greedy * (self.horizon // partial_interval_routes))
        self.print_in_console(
            f"Nb routes from initial generation: {len(routes)}")
        self.print_in_console(f"Nb routes per run: {self.nb_routes}")

        return dict(enumerate(routes))

    def generate_new_routes(self):
        """
        Generates new shifts based on the values of the artificial variables , i.e. based on the ideal deliveries
        :return: A dictionary whose keys are the indices of the shifts and whose values are instances of RouteLabel
        """
        self.print_in_console("Generating new routes at: ",
                              datetime.now().strftime("%H:%M:%S"))

        new_routes = self.routes_generator.generate_new_routes(
            self.artificial_quantities, make_clusters=False)

        new_routes = [
            route for route in new_routes
            if route.start + MIPModel.duration_route(route) <= self.horizon
        ]

        enumerated_new_routes = list(
            enumerate(new_routes,
                      max(self.routes.keys()) + 1))

        self.print_in_console(
            f"Generated {len(enumerated_new_routes)} new routes")

        self.routes = OrderedDict(enumerated_new_routes +
                                  list(self.routes.items()))
        self.unused_routes = OrderedDict(enumerated_new_routes +
                                         list(self.unused_routes.items()))

    def new_model(self, used_routes, previous_routes_infos,
                  artificial_variables):
        self.print_in_console("Generating new model at: ",
                              datetime.now().strftime("%H:%M:%S"))
        model = pl.LpProblem("Roadef", pl.LpMinimize)
        self.create_variables(used_routes, artificial_variables)
        self.warm_start_variables(previous_routes_infos)
        model = self.create_constraints(model, used_routes,
                                        artificial_variables)
        return model

    def warm_start_variables(self, previous_routes_infos):
        previous_routes_infos_s = set(previous_routes_infos)
        for k, v in self.route_var.items():
            v.setInitialValue(k in previous_routes_infos_s)

    def create_variables(self, used_routes, artificial_variables=False):
        # Indices
        ind_td_routes = self.get_td_routes(used_routes)
        ind_customers_hours = self.get_customers_hours()

        # Variables : route
        self.route_var = pl.LpVariable.dicts("route", ind_td_routes, 0, 1,
                                             pl.LpBinary)
        self.route_var = SuperDict(self.route_var)

        self.print_in_console("Var 'route'")

        # Variables : Artificial Quantity
        if artificial_variables:
            self.artificial_quantities_var = pl.LpVariable.dicts(
                "ArtificialQuantity", ind_customers_hours, None, 0)
            self.artificial_binary_var = pl.LpVariable.dicts(
                "ArtificialBinary", ind_customers_hours, 0, 1, pl.LpBinary)
        else:
            self.artificial_quantities_var = {}
            self.artificial_binary_var = {}

        self.artificial_quantities_var = SuperDict(
            self.artificial_quantities_var)
        self.artificial_binary_var = SuperDict(self.artificial_binary_var)

        self.print_in_console("Var 'ArtificialQuantity'")

        # Variables : Inventory
        _capacity = lambda i: self.instance.get_customer_property(
            i, "Capacity")
        self.inventory_var = {(i, h): pl.LpVariable(f"Inventory{i, h}", 0,
                                                    _capacity(i))
                              for (i, h) in self.get_var_inventory_domain()}
        self.inventory_var = SuperDict(self.inventory_var)
        self.print_in_console("Var 'inventory'")

        # Variables : quantity
        for (r, i, tr, k) in self.get_var_quantity_s_domain(used_routes):
            self.quantity_var[i, r, tr,
                              k] = pl.LpVariable(f"quantity{i, r, tr, k}",
                                                 lowBound=0)

        for (r, i, tr, k) in self.get_var_quantity_p_domain(used_routes):
            self.quantity_var[i, r, tr,
                              k] = pl.LpVariable(f"quantity{i, r, tr, k}",
                                                 upBound=0)
        self.print_in_console("Var 'quantity'")

        # Variables : TrailerQuantity
        _capacity = lambda tr: self.instance.get_trailer_property(
            tr, "Capacity")
        self.trailer_quantity_var = {
            (tr, h): pl.LpVariable(
                f"TrailerQuantity{tr, h}",
                lowBound=0,
                upBound=_capacity(tr),
            )
            for (tr, h) in self.get_var_trailer_quantity_domain()
        }
        self.trailer_quantity_var = SuperDict(self.trailer_quantity_var)
        self.print_in_console("Var 'TrailerQuantity'")

    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

    def select_routes(self, nb=500):
        """
        Selects the indicated number of shifts from self.unused_routes and deletes them from self.unused_routes
        :return: A dictionary whose keys are the indices of the shifts and whose values are instances of RouteLabel
        """
        self.print_in_console("Selecting routes at: ",
                              datetime.now().strftime("%H:%M:%S"))
        selected_routes = dict()
        nb = min(nb, len(self.unused_routes))

        for r in list(self.unused_routes.keys())[0:nb]:
            selected_routes[r] = self.unused_routes[r]
            del self.unused_routes[r]
        self.print_in_console(selected_routes)
        return selected_routes

    def check_and_save(self, current_round):
        """Checks the solution and, in some cases, saves the log in a file"""
        self.print_in_console("Checking solution at: ",
                              datetime.now().strftime("%H:%M:%S"))
        self.calculate_inventories()
        check_dict = self.check_solution()
        check_log = self.generate_log(check_dict)

        if self.save_results:
            with open(f"res/log-{self.start_time_string}-R{current_round}.txt",
                      "w") as fd2:
                fd2.write(f"Objective: {self.get_objective()}\n")
                fd2.write(check_log)

        self.print_in_console(check_log)
        self.print_in_console(f"Objective: {self.get_objective()}")

    def cost(self, r, tr, dr):
        """Returns the cost of performing a shift with given trailer and given driver"""
        route = self.routes[r]
        total_time = route.visited[-1][1] - route.visited[0][1]
        time_cost = self.instance.get_driver_property(dr, "TimeCost")
        total_dist = 0

        for i in range(1, len(route.visited)):
            total_dist += self.instance.get_distance_between(
                route.visited[i - 1][0], route.visited[i][0])
        dist_cost = self.instance.get_trailer_property(tr, "DistanceCost")
        return total_time * time_cost + total_dist * dist_cost

    def duration(self, r):
        """Returns the duration of the route of index r"""
        route = self.routes[r]
        return (route.visited[-1][1] - route.visited[0][1]) / 60

    @staticmethod
    def duration_route(route):
        """Returns the duration of the given route"""
        return (route.visited[-1][1] - route.visited[0][1]) / 60

    def get_nb_visits(self, i, r):
        """Returns the number of visits to location i on the route of index r"""
        return self.locations_in[r].count(i)

    def get_hour_of_visit(self, i, r, k):
        """Returns the time at which route r visits the location i for the k_th time"""
        route = self.routes[r]
        hour = None
        th = 1
        for step in route.visited:
            if step[0] == i and th == k:
                hour = step[1] / 60
            if step[0] == i:
                th += 1
        return floor(route.start + hour)

    def get_k_visit_hour(self, i, h, r, k):
        """Returns 1 if route r visits location i for the k_th time at hour h"""
        route = self.routes[r]
        th = 1
        visit = 0
        for step in route.visited:
            if step[0] == i and th == k:
                if floor(route.start + step[1] / 60) == h:
                    visit = 1
            if step[0] == i:
                th += 1
        return visit

    def initialize_parameters(self, used_routes):
        """Caches the values of some of the functions to avoid recalculating each time"""
        self.locations_in = dict(
            (r, self.get_locations_in_r(self.routes[r])) for r in used_routes)
        self.unique_locations_in = dict(
            (r, self.get_unique_locations_in(self.routes[r]))
            for r in used_routes)
        self.nb_visits = dict(((i, r), self.get_nb_visits(i, r))
                              for i in self.all_locations for r in used_routes)
        self.hour_of_visit = dict(((i, r, k), self.get_hour_of_visit(i, r, k))
                                  for i in self.all_locations
                                  for r in used_routes
                                  for k in range(1, self.nb_visits[i, r] + 1))
        self.k_visit_hour = dict(
            ((i, h, r, k), self.get_k_visit_hour(i, h, r, k))
            for i in self.all_locations for r in used_routes
            for h in self.range_hours
            for k in range(1, self.nb_visits[i, r] + 1))

    def visit_before(self, i, r, k, rp, kp):
        """
        :return: 1 if route r visits i before rp does, 0 otherwise
        """
        h = self.hour_of_visit[i, r, k]
        hp = self.hour_of_visit[i, rp, kp]
        if h is None or hp is None:
            return 0
        elif h < hp:
            return 1
        return 0

    def visit_before_on_route(self, r, i, k, ip, kp):
        """
        :return: 1 if route r visits i before ip, 0 otherwise
        """
        h = self.hour_of_visit[i, r, k]
        hp = self.hour_of_visit[ip, r, kp]
        if h is None or hp is None:
            return 0
        elif h < hp:
            return 1
        return 0

    def runs_at_hour(self, r, h):
        """
        :return: 1 if route r is being 'executed' at hour h
        """
        route = self.routes[r]
        return floor(route.start) <= h <= route.start + self.duration(r)

    def blocks_driver_at_hour(self, r, dr, h):
        """
        :return: 1 if at hour h, the driver dr would be on route r or resting after shift r
            if he was assigned to route r
        """
        route = self.routes[r]
        return (
            floor(route.start) <= h <=
            (route.start + self.duration(r) +
             self.instance.get_driver_property(dr, "minInterSHIFTDURATION") /
             60))

    def k_position(self, i, r, k):
        """
        :return: the position of the k_th visit at location i on route r
        """
        route = self.routes[r]
        th = 1
        pos = None
        for j, step in enumerate(route.visited):
            if step[0] == i and th == k:
                pos = j
            if step[0] == i:
                th += 1
        return pos

    def to_solution(self, model, used_routes, current_round):
        """Converts the variables returned by the solver into an instance of Solution"""
        self.print_in_console("Converting to Solution at: ",
                              datetime.now().strftime("%H:%M:%S"))
        self.solution = Solution(dict())
        shifts = dict()
        self.artificial_quantities = dict()

        if self.save_results:
            with open(
                    f"res/solution-{self.start_time_string}-R{current_round}.txt",
                    "w") as fd:
                for var in model.variables():
                    fd.write(var.name + f": {var.varValue}\n")

        selected_r_tr_dr = self.route_var.vfilter(
            lambda v: round(pl.value(v), 2) == 1).keys_tl()

        for (r, tr, dr) in selected_r_tr_dr:
            label = used_routes[r]
            route = [
                dict(
                    location=step[0],
                    departure=step[1] + 60 * label.start,
                    layover_before=0,
                    driving_time_before_layover=0,
                ) for step in label.visited
            ]

            for s, step in enumerate(route):
                if self.is_customer_or_source(step["location"]):
                    route[s]["arrival"] = step[
                        "departure"] - self.instance.get_location_property(
                            step["location"], "setupTime")
                elif self.is_base(step["location"]):
                    route[s]["arrival"] = step["departure"]
                    route[s]["quantity"] = 0
            shifts[r] = dict(
                driver=dr,
                trailer=tr,
                route=route,
                id_shift=r,
                departure_time=60 * label.start,
            )
        self.artificial_quantities = dict(
            self.artificial_quantities_var.vfilter(lambda v: round(
                pl.value(v), 3) != 0).vapply(lambda v: round(pl.value(v), 3)))
        nb_artificial_deliveries = len(self.artificial_quantities)
        self.print_in_console("Nb Artificial deliveries: ",
                              nb_artificial_deliveries)

        delivered_quantities = self.quantity_var.vapply(
            lambda v: round(pl.value(v), 3))
        for (i, r, tr, k), val in delivered_quantities.items():
            if shifts.get(r, None) is None:
                continue
            if shifts[r]["trailer"] != tr:
                continue
            th = 1

            for j, step in enumerate(shifts[r]["route"]):
                if step["location"] == i and th == k:
                    shifts[r]["route"][j]["quantity"] = val
                if step["location"] == i:
                    th += 1
        trailer_quantities = self.trailer_quantity_var.vapply(
            lambda v: round(pl.value(v), 3))

        for (tr, h), val in trailer_quantities.items():
            for r, route in shifts.items():
                if route["trailer"] != tr:
                    continue
                if route["departure_time"] // 60 != h + 1:
                    continue
                shifts[r]["initial_quantity"] = val

        self.solution = Solution(shifts)

    def set_final_id_shifts(self):
        """Changes the indices of all the shifts so that the indices start from 1 and count by one"""
        new_dict = dict()
        id_shift = 1
        for r, route in self.solution.get_id_and_shifts():
            new_dict[id_shift] = route
            new_dict[id_shift]["id_shift"] = id_shift
            id_shift += 1
        self.solution = Solution(new_dict)

    def post_process(self):
        """
        Removes steps from routes if the quantities delivered/loaded are 0
        """
        new_solution = dict()
        for (id_shift, shift) in self.solution.get_id_and_shifts():
            shift_copy = pickle.loads(pickle.dumps(shift, -1))
            removed = 0

            for (id_step, step) in enumerate(shift["route"]):
                new_id_step = id_step - removed
                if step["quantity"] == 0 and step["location"] != 0:
                    del shift_copy["route"][new_id_step]
                    removed += 1
            if removed == 0:
                new_solution[id_shift] = shift
                continue

            for (id_step, step) in enumerate(shift_copy["route"]):
                if step["location"] == 0:
                    continue
                time_from_last = self.instance.get_time_between(
                    shift_copy["route"][id_step - 1]["location"],
                    step["location"])
                shift_copy["route"][id_step]["arrival"] = (
                    shift_copy["route"][id_step - 1]["departure"] +
                    time_from_last +
                    (self.instance.get_driver_property(shift["driver"],
                                                       "LayoverDuration") *
                     shift_copy["route"][id_step]["layover_before"]))
                shift_copy["route"][id_step][
                    "departure"] = shift_copy["route"][id_step][
                        "arrival"] + self.instance.get_location_property(
                            step["location"], "setupTime")
                shift_copy["route"][id_step]["cumulated_driving_time"] = (
                    shift_copy["route"][id_step - 1]["cumulated_driving_time"]
                    + time_from_last)

            if len(shift_copy["route"]) > 2:
                new_solution[id_shift] = shift_copy
        self.solution = Solution(new_solution)
        self.set_final_id_shifts()
        self.calculate_inventories()

    def get_td_routes(self, used_routes):
        """Returns a list of every possible truple (index_route, index_trailer, index_driver)"""
        return list(
            itertools.product(
                used_routes,
                self.instance.get_id_trailers(),
                self.instance.get_id_drivers(),
            ))

    def get_customers_hours(self):
        return list(itertools.product(self.all_customers, self.range_hours))

    def get_var_inventory_domain(self):
        return [(i, h) for i in self.all_customers
                for h in self.range_hours + [-1]]

    def get_var_trailer_quantity_domain(self):
        return [(tr, h) for tr in self.instance.get_id_trailers()
                for h in self.range_hours + [-1]]

    def get_var_quantity_s_domain(self, used_routes):
        return [(r, i, tr, k) for r in used_routes for i in self.all_sources
                for tr in self.instance.get_id_trailers()
                for k in range(1, self.nb_visits[i, r] + 1)]

    def get_var_quantity_p_domain(self, used_routes):
        return [(r, i, tr, k) for r in used_routes for i in self.all_customers
                for tr in self.instance.get_id_trailers()
                for k in range(1, self.nb_visits[i, r] + 1)]

    def get_sum_c4_domain(self, used_routes):
        return [(r, route, dr) for r, route in used_routes.items()
                for dr in self.instance.get_id_drivers()]

    def get_sum_c5_domain(self, used_routes):
        return [(r, route, tr) for r, route in used_routes.items()
                for tr in self.instance.get_id_trailers()]

    def get_sum_c9_domain(self, used_routes):
        return [(r, i, k) for r in used_routes
                for i in self.unique_locations_in[r]
                for k in range(1, self.nb_visits[i, r] + 1)]

    def get_sum_c7_c12_domain(self, used_routes, i):
        return [(r, tr, k) for r in used_routes
                for tr in self.instance.get_id_trailers()
                for k in range(1, self.nb_visits[i, r] + 1)]

    def get_sum_c11_c14_domain(self, i, r, k):
        return [(j, kp) for j in range(1, self.k_position(i, r, k))
                for kp in range(
                    1, self.nb_visits[self.routes[r].visited[j][0], r] + 1)]

    def get_c4_c9_domain(self):
        return [(tr, h) for tr in self.instance.get_id_trailers()
                for h in self.range_hours]

    def get_c5_domain(self):
        return [(dr, h) for dr in self.instance.get_id_drivers()
                for h in self.range_hours]

    def get_c10_domain(self, used_routes):
        return [(r, i, tr, k) for r in used_routes for i in self.all_customers
                for tr in self.instance.get_id_trailers()
                for k in range(1, self.nb_visits[i, r] + 1)]

    def get_c11_c12_domain(self, used_routes):
        return [(r, route, i, tr, k) for r, route in used_routes.items()
                for i in self.unique_customers_in(route)
                for tr in self.instance.get_id_trailers()
                for k in range(1, self.nb_visits[i, r] + 1)]

    def get_c13_domain(self, used_routes):
        return [(r, i, tr, k) for r in used_routes
                for i in self.instance.get_id_sources()
                for tr in self.instance.get_id_trailers()
                for k in range(1, self.nb_visits[i, r] + 1)]

    def get_c14_domain(self, used_routes):
        return [(r, route, i, tr, k) for r, route in used_routes.items()
                for i in self.unique_sources_in(route)
                for tr in self.instance.get_id_trailers()
                for k in range(1, self.nb_visits[i, r] + 1)]

    # Locations visited by r
    def get_locations_in_r(self, route):
        """
        :return: A list of all the sources and customers in the given route
        """
        return [step[0] for step in route.visited if not self.is_base(step[0])]

    def get_unique_locations_in(self, route):
        """
        :return: A unique list of all the sources and customers in the given route
        """
        return TupList([
            step[0] for step in route.visited if not self.is_base(step[0])
        ]).unique()

    def unique_customers_in(self, route):
        """
        :return: A unique list of all the customers in the given route
        """
        return TupList([
            step[0] for step in route.visited if self.is_customer(step[0])
        ]).unique()

    def unique_sources_in(self, route):
        """
        :return: A unique list of all the sources in the given route
        """
        return TupList([
            step[0] for step in route.visited if self.is_source(step[0])
        ]).unique()

    @property
    def all_locations(self):
        """Returns the indices of all the sources and customers"""
        return list(self.instance.get_id_sources()) + list(
            self.instance.get_id_customers())

    @property
    def all_customers(self):
        """Returns the indices of all the customers"""
        return list(self.instance.get_id_customers())

    @property
    def all_sources(self):
        """Returns the indices of all the sources"""
        return list(self.instance.get_id_sources())

    def print_in_console(self, *args):
        if self.print_log:
            print(*args)
Exemplo 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
Exemplo n.º 4
0
def drop_props_get_values(dictionary: SuperDict, props: list):
    return dictionary.vapply(
        lambda v: v.kfilter(lambda k: k not in props)).values_tl()