Esempio n. 1
0
    def check_shift_16(self, shift, id_shift):
        """
        returns {"exceeds": {(id_shift, id_location): quantity, ... } for
                        operation with quantity above trailer's capacity
                 "too_low": {(id_shift, id_location): quantity, ... } for
                        operation with quantity < min_quantity
                }
        """
        check = SuperDict(exceeds=SuperDict(), too_low=SuperDict())
        for i in range(1, len(shift["route"]) + 1):
            operation = shift["route"][i - 1]
            location = operation["location"]

            if not self.is_customer(location):
                continue

            call_in = self.instance.get_customer_property(location, "callIn")
            if call_in == 0:
                capacity = self.instance.get_customer_property(
                    location, "Capacity")
                min_ope_quantity = self.instance.get_customer(location).get(
                    "MinOperationQuantity", 0)
                if -operation["quantity"] > capacity:
                    check["exceeds"][(id_shift,
                                      location)] = operation["quantity"]
                elif -operation["quantity"] < min_ope_quantity:
                    check["too_low"][(id_shift,
                                      location)] = operation["quantity"]
        check = check.vfilter(lambda v: len(v) != 0)
        return check
Esempio n. 2
0
    def check_shifts(self):
        check = SuperDict(
            c_02_timeline=SuperDict(),
            c_03_wrong_index=SuperDict(),
            c_03_setup_times=SuperDict(),
            c_04_customer_TW=SuperDict(),
            c_05_sites_accessible=SuperDict(),
            c_0607_inventory_trailer_negative=SuperDict(),
            c_0607_inventory_trailer_above_capacity=SuperDict(),
            c_0607_inventory_trailer_final_inventory=SuperDict(),
            c_0607_inventory_trailer_initial_inventory=SuperDict(),
            c_11_quantity_delivered=SuperDict(),
            c_16_customer_tank_exceeds=SuperDict(),
            c_16_customer_tank_too_low=SuperDict(),
            driver_and_trailer=SuperDict(),
        )

        for shift in self.solution.get_all_shifts():
            driver = shift.get("driver", None)
            trailer = shift.get("trailer", None)
            id_shift = shift.get("id_shift", None)
            if driver is None or trailer is None:
                check["driver_and_trailer"][id_shift] = 1
                continue
            check["c_02_timeline"].update(
                self.check_shift_02(shift, id_shift, driver))

            res_check_c03 = self.check_shift_03(
                shift, id_shift).vfilter(lambda v: len(v) != 0)
            check["c_03_wrong_index"].update(
                res_check_c03.get("wrong_index", []))
            check["c_03_setup_times"].update(
                res_check_c03.get("setup_time", []))
            check["c_04_customer_TW"].update(
                self.check_shift_04(shift, id_shift))
            check["c_05_sites_accessible"].update(
                self.check_shift_05(shift, id_shift, trailer))
            check["c_11_quantity_delivered"].update(
                self.check_shift_11(shift, id_shift))
            res_check_c16 = self.check_shift_16(
                shift, id_shift).vfilter(lambda v: len(v) != 0)
            check["c_16_customer_tank_exceeds"].update(
                res_check_c16.get("exceeds", []))
            check["c_16_customer_tank_too_low"].update(
                res_check_c16.get("too_low", []))

        checks_0607 = self.check_shift_0607().vfilter(lambda v: len(v) != 0)
        check["c_0607_inventory_trailer_negative"] = checks_0607.get(
            "inventory_negative", [])
        check["c_0607_inventory_trailer_above_capacity"] = checks_0607.get(
            "above_capacity", [])
        check["c_0607_inventory_trailer_final_inventory"] = checks_0607.get(
            "final_inventory", [])
        check["c_0607_inventory_trailer_initial_inventory"] = checks_0607.get(
            "initial_inventory", [])
        return check.vfilter(lambda v: len(v))
Esempio n. 3
0
    def check_shift_0607(self):
        """
        returns {"negative": {(id_shift, id_location): quantity, ... } for operation resulting in negative trailer inventory
                 "above_capacity": {(id_shift, id_location): quantity, ... } for operation resulting in excessive trailer inventory
                 "final_inventory": {id_shift: 1, ...} for shifts with inconsistent final inventory
                 "initial_inventory": {id_shift: 1, ...} for shifts with inconsistent initial inventory }
        """
        check = SuperDict(
            negative=SuperDict(),
            above_capacity=SuperDict(),
            final_inventory=SuperDict(),
            initial_inventory=SuperDict(),
        )
        last_trailer_quantity = [0] * len(self.instance.get_id_trailers())
        aux_info_shift = self.get_aux_info_shift()

        for trailer in self.instance.get_id_trailers():
            last_trailer_quantity[
                trailer] = self.instance.get_trailer_property(
                    trailer, "InitialQuantity")

        for shift in sorted(list(self.solution.get_all_shifts()),
                            key=lambda x: x["departure_time"]):
            driver = shift.get("driver", None)
            trailer = shift.get("trailer", None)
            id_shift = shift.get("id_shift", None)
            if driver is None or trailer is None:
                return check
            # Constraints (06)
            last_quantity = shift["initial_quantity"]

            for operation in shift["route"]:
                quantity = last_quantity + operation["quantity"]
                location = operation["location"]
                if round(quantity, 2) < 0:
                    check["negative"][(id_shift, location)] = quantity
                elif round(quantity, 2) > self.instance.get_trailer_property(
                        trailer, "Capacity"):
                    check["above_capacity"][(id_shift, location)] = quantity

                last_quantity = quantity
            # Constraints (07)
            if round(aux_info_shift[id_shift]["final_inventory"], 2) != round(
                    last_quantity, 2):
                check["final_inventory"][id_shift] = 1
            if round(last_trailer_quantity[trailer], 2) != round(
                    shift["initial_quantity"], 2):
                check["initial_inventory"][id_shift] = 1
            last_trailer_quantity[trailer] = last_quantity
        check = check.vfilter(lambda v: len(v) != 0)
        return check
Esempio n. 4
0
    def check_resources(self):
        check = SuperDict(
            res_dr_01_intershift=SuperDict(),
            res_dr_03_max_duration=SuperDict(),
            res_dr_08_driver_TW=SuperDict(),
            res_tl_01_shift_overlaps=SuperDict(),
            res_tl_03_compatibility_dr_tr=SuperDict(),
        )
        end_of_last_shifts_drivers = [0] * self.nb_drivers
        end_of_last_shifts_trailer = [0] * self.nb_trailers
        aux_info_shift = self.get_aux_info_shift()

        for shift in sorted(list(self.solution.get_all_shifts()),
                            key=lambda x: x["departure_time"]):
            driver = shift.get("driver", None)
            trailer = shift.get("trailer", None)
            id_shift = shift.get("id_shift", None)
            if driver is None or trailer is None:
                continue
            # DRIVERS
            if self.check_resources_dr_01(shift, driver,
                                          end_of_last_shifts_drivers[driver]):
                check["res_dr_01_intershift"][id_shift, driver] = 1

            check_03 = self.check_resources_dr_03(shift, driver)
            if check_03 is not None:
                check["res_dr_03_max_duration"][id_shift, driver] = check_03

            if self.check_resources_dr_08(shift, id_shift, driver):
                check["res_dr_08_driver_TW"][id_shift] = 1

            # TRAILERS
            if self.check_resources_tl_01(shift,
                                          end_of_last_shifts_trailer[trailer]):
                check["res_tl_01_shift_overlaps"][id_shift] = 1

            if self.check_resources_tl_03(driver, trailer):
                check["res_tl_03_compatibility_dr_tr"][id_shift] = 1

            #
            end_of_last_shifts_trailer[trailer] = aux_info_shift[id_shift][
                "arrival_time"]
            end_of_last_shifts_drivers[driver] = aux_info_shift[id_shift][
                "arrival_time"]
        return check.vfilter(lambda v: len(v))
Esempio n. 5
0
    def check_sites(self):
        """
        returns {"site_inventory_negative": {(location, time): inventory, ... } for inventories > tank_capacity
                 "site_inventory_exceeds": {(location, time): inventory, ... } for  negative inventories
                 "site_doesntexist": {location: 1}   for nonexistent locations
                }
        """
        check = SuperDict(
            site_inventory_negative=SuperDict(),
            site_inventory_exceeds=SuperDict(),
            site_doesntexist=SuperDict(),
        )
        operation_quantities = []

        for location in range(1 + self.nb_sources + self.nb_customers):
            operation_quantities.append([0] * self.horizon)

        site_inventories = self.calculate_inventories()
        for site_inventory in site_inventories.values():
            location = site_inventory["location"]
            if not self.is_valid_location(location):
                check["site_doesntexist"][location] = 1
                continue
            if location <= 1 + self.nb_sources:
                continue
            call_in = self.instance.get_customer_property(location, "callIn")
            if call_in == 1:
                continue
            for i in range(self.horizon):
                if round(site_inventory["tank_quantity"][i], 3) < 0:
                    check["site_inventory_negative"][
                        location, i] = site_inventory["tank_quantity"][i]
                if round(site_inventory["tank_quantity"][i],
                         3) > self.instance.get_customer_property(
                             location, "Capacity"):
                    check["site_inventory_exceeds"][
                        location, i] = site_inventory["tank_quantity"][i]
        check = check.vfilter(lambda v: len(v) != 0)
        return check
Esempio n. 6
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)
Esempio n. 7
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
Esempio n. 8
0
class PeriodicMIP(MIPModel):
    def __init__(self, instance, solution=None):
        super().__init__(instance, solution)
        self.log = ""
        self.solver = "Periodic 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.Hmin = 0
        self.Hmax = self.horizon
        self.resolution_interval = 180
        self.resolve_margin = 48
        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.final_routes = None
        self.coef_part_inventory_conservation = 0.8
        self.coef_inventory_conservation = 1
        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()

    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.coef_part_inventory_conservation = config.get(
            "partialInventoryConservation", self.coef_inventory_conservation
        )
        solver_name = self.get_solver(config)

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

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

        used_routes = dict()

        self.Hmin = 0
        self.Hmax = min(self.resolution_interval, self.horizon)
        while self.Hmin < self.horizon and self._get_remaining_time() > 0:
            self.print_in_console(
                f"=================== Hmin = {self.Hmin} ========================"
            )
            self.print_in_console(
                f"=================== Hmax = {self.Hmax} ========================"
            )
            current_round = 0

            new_routes = self.generate_initial_routes()
            self.routes = dict(list(self.routes.items()) + list(new_routes.items()))
            previous_value = None
            self.unused_routes = pickle.loads(pickle.dumps(new_routes, -1))

            self.print_in_console(
                "=================== ROUND 0 ========================"
            )
            self.print_in_console(
                "Initial empty solving at: ", datetime.now().strftime("%H:%M:%S")
            )

            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):
                return dict(
                    solver=solver_name,
                    gapRel=0.05,
                    timeLimit=min(100.0, self._get_remaining_time()),
                    msg=self.print_log,
                    warmStart=(current_round != 1),
                )

            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))

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

            self.Hmax = min(self.Hmax + self.resolution_interval, self.horizon)
            self.Hmin += self.resolution_interval

        self.set_final_id_shifts()
        self.post_process()

        self.final_routes = used_routes
        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 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,
            nb_select_at_random_greedy=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_greedy = routes[0:nb_greedy]
        other_routes = routes[nb_greedy:]

        routes = [
            route.copy().get_label_with_start(start)
            for offset in range(0, partial_interval_routes, self.interval_routes)
            for route in routes_greedy
            for start in range(
                max(self.Hmin - 72, 0) + offset, self.Hmax, partial_interval_routes
            )
            if start + PeriodicMIP.duration_route(route) < self.Hmax
        ] + [
            route.copy().get_label_with_start(start)
            for offset in range(0, partial_interval_routes, self.interval_routes)
            for route in other_routes
            for start in range(
                max(self.Hmin - 72, 0) + offset, self.Hmax, partial_interval_routes
            )
            if start + PeriodicMIP.duration_route(route) < self.Hmax
        ]
        self.print_in_console(routes)

        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}")

        if len(self.routes) != 0:
            start_id = max(list(self.routes.keys())) + 1
        else:
            start_id = 0
        self.print_in_console(dict(enumerate(routes, start_id)))
        return dict(enumerate(routes, start_id))

    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=True
        )

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

        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, previous_routes_infos, artificial_variables)
        model = self.create_constraints(model, used_routes, artificial_variables)
        return model

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

        # Initial quantities from previous solutions
        initial_quantities = dict()
        for r, route in self.solution.get_id_and_shifts():
            k = defaultdict(int)
            for step in route["route"]:
                if step["location"] == 0:
                    continue
                k[step["location"]] += 1
                initial_quantities[
                    (
                        step["location"],
                        r,
                        self.solution.get_shift_property(r, "trailer"),
                        k[step["location"]],
                    )
                ] = step["quantity"]

        # Variables : route
        self.route_var = pl.LpVariable.dicts("route", ind_td_routes, 0, 1, pl.LpBinary)
        previous_routes_infos_s = set(previous_routes_infos)
        for k, v in self.route_var.items():
            v.setInitialValue(k in previous_routes_infos_s)
            if self.routes[k[0]].start < self.Hmin - self.resolve_margin:
                v.fixValue()
        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
            )

            for (i, h) in ind_customers_hours:
                if h < self.Hmin - self.resolve_margin:
                    self.artificial_binary_var[i, h].setInitialValue(0)
                    self.artificial_binary_var[i, h].fixValue()
            self.artificial_quantities_var = SuperDict(self.artificial_quantities_var)
            self.artificial_binary_var = SuperDict(self.artificial_binary_var)
        else:
            self.artificial_quantities_var = SuperDict()
            self.artificial_binary_var = SuperDict()
        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}", 0)
            if initial_quantities.get((i, r, tr, k), None) is None:
                continue
            self.quantity_var[i, r, tr, k].setInitialValue(
                initial_quantities[(i, r, tr, k)]
            )
            """if self.routes[r].start < self.Hmin - self.resolve_margin:
                self.variables["quantity"][(i, r, tr, k)].fixValue() """

        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
            )
            if initial_quantities.get((i, r, tr, k), None) is None:
                continue
            self.quantity_var[i, r, tr, k].setInitialValue(
                initial_quantities[(i, r, tr, k)]
            )
            """if self.routes[r].start < self.Hmin - self.resolve_margin:
                self.variables["quantity"][(i, r, tr, k)].fixValue() """
        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]
                    for (r, tr, k) in _sum_c7_c12_domain[i]
                    if self.k_visit_hour[(i, h, r, k)]
                )
                - 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
        if self.Hmax >= self.horizon:
            model += (
                pl.lpSum(
                    self.coef_inventory_conservation * self.inventory_var[i, -1]
                    - self.inventory_var[i, self.Hmax - 1]
                    for i in self.instance.get_id_customers()
                )
                <= 0
            ), f"C15"
            self.print_in_console("Added (15)")
        else:
            model += (
                pl.lpSum(
                    self.coef_part_inventory_conservation * self.inventory_var[i, -1]
                    - self.inventory_var[i, self.Hmax - 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 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, range(self.Hmax)))

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

    def get_var_trailer_quantity_domain(self):
        return [
            (tr, h)
            for tr in self.instance.get_id_trailers()
            for h in list(range(self.Hmax)) + [-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 range(self.Hmax)
        ]

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

    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)
        ]
Esempio n. 9
0
class SchemaGenerator:
    def __init__(self,
                 path,
                 output_path=None,
                 ignore_files=None,
                 leave_bases=False):

        self.path = path
        self.tmp_path = os.path.join(os.getcwd(), "tmp_files")
        self.output_path = output_path or "./output_schema.json"
        self.ignore_files = ignore_files or []
        self.leave_bases = leave_bases or False
        self.parents = dict()
        self.data = SuperDict()
        self.model_table = dict()
        self.table_model = dict()

    def main(self):
        os.mkdir(self.tmp_path)

        copy_tree(self.path, self.tmp_path)

        files = (TupList(os.listdir(self.tmp_path)).vfilter(
            lambda v: v.endswith(".py") and v != "__init__.py" and v !=
            "__pycache__" and v not in self.ignore_files).vapply(
                lambda v: (os.path.join(self.tmp_path, v), v[:-3])))

        self.mock_packages(files)

        self.parse(files)

        self.inherit()

        schema = self.to_schema()

        with open(self.output_path, "w") as fd:
            json.dump(schema, fd, indent=2)
        self.clear()
        return 0

    def mock_packages(self, files):
        # Mocking all relative imports
        for file_path, file_name in files:
            with open(file_path, "r") as fd:
                text = fd.read()
            parents = re.findall(r"class (.+)\((.+)\):", text)
            for cl, parent in parents:
                self.parents[cl] = parent.replace(" ", "")
            libs = re.findall(r"from ((\.+\w+)+) import (\w+)", text)
            for lib in libs:
                text = text.replace(lib[0], "mockedpackage")
            with open(file_path, "w") as fd:
                fd.write(text)

        sys.modules["mockedpackage"] = MagicMock()

    def parse(self, files):
        forget_keys = ["created_at", "updated_at", "deleted_at"]
        db = SQLAlchemy()
        try:
            for file_path, file_name in files:

                spec = importlib.util.spec_from_file_location(
                    file_name, file_path)
                mod = importlib.util.module_from_spec(spec)

                spec.loader.exec_module(mod)

                models = SuperDict(
                    mod.__dict__).kfilter(lambda k: k in self.parents)
                for model in models:
                    if isinstance(models[model], MagicMock):
                        # Models that inherit from other models that are relatively imported
                        if not isinstance(
                                mod.__dict__[model]._mock_return_value, dict):
                            continue
                        props = mod.__dict__[model]._mock_return_value
                    elif mod.__dict__[model].__dict__.get("__abstract__"):
                        # BaseDataModel
                        props = mod.__dict__[model].__dict__
                        self.parents[model] = None
                    else:
                        # Models that inherit from other models that are imported from libraries
                        self.parents[model] = None
                        tmp = mod.__dict__[model].__dict__
                        props = {"__tablename__": tmp.get("__tablename__")}
                        for col in tmp["__table__"]._columns:
                            props[col.__dict__["key"]] = next(
                                iter(col.proxy_set))
                    table_name = props.get("__tablename__", model)
                    self.data[table_name] = SuperDict(type="array",
                                                      items=dict(
                                                          properties=dict(),
                                                          required=[]))
                    if not props.get("__tablename__") and not self.leave_bases:
                        self.data[table_name]["remove"] = True
                    self.model_table[model] = table_name
                    self.table_model[table_name] = model
                    for key, val in props.items():
                        if key in forget_keys:
                            continue
                        elif isinstance(val, db.Column):
                            type_converter = {
                                db.String: "string",
                                TEXT: "string",
                                JSON: "object",
                                Integer: "integer",
                                db.Integer: "integer",
                                db.Boolean: "boolean",
                                db.SmallInteger: "integer",
                                db.Float: "number",
                            }
                            type_col = "null"
                            for possible_type, repr_type in type_converter.items(
                            ):
                                if isinstance(val.type, possible_type):
                                    type_col = repr_type
                            if type_col == "null":
                                raise Exception("Unknown column type")

                            self.data[table_name]["items"]["properties"][
                                key] = SuperDict(type=type_col)
                            if val.foreign_keys:
                                fk = list(val.foreign_keys)[0]
                                self.data[table_name]["items"]["properties"][
                                    key]["foreign_key"] = fk._colspec
                            if not val.nullable:
                                self.data[table_name]["items"][
                                    "required"].append(key)

            db.session.close()
        except Exception as err:
            print(err)

    def inherit(self):
        all_classes = set(self.parents.keys())
        not_treated = set(all_classes)
        treated = {"db.Model"}
        while not_treated:
            for model in not_treated:
                parent = self.parents[model]
                if parent is None:
                    treated.add(model)
                    continue
                if parent not in treated:
                    continue
                treated.add(model)
                if parent == "db.Model":
                    continue
                table_name = self.model_table[model]
                parent_props = self.data[
                    self.model_table[parent]]["items"]["properties"]
                parent_requirements = self.data[
                    self.model_table[parent]]["items"]["required"]
                self.data[table_name]["items"]["properties"] = SuperDict(
                    **parent_props,
                    **self.data[table_name]["items"]["properties"])
                self.data[table_name]["items"][
                    "required"] += parent_requirements
            not_treated -= treated
        if not self.leave_bases:
            self.data = self.data.vfilter(lambda v: not v.get("remove", False))

    def clear(self):
        if os.path.isdir(self.tmp_path):
            shutil.rmtree(self.tmp_path)

    def to_schema(self):
        return {
            "$schema": "http://json-schema.org/schema#",
            "type": "object",
            "properties": self.data,
            "required": list(self.data.keys()),
        }