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
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))
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
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))
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
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)
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
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) ]
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()), }