def to_tsplib95(self): arcs = TupList(self.data["arcs"]) nodes = (arcs.take("n1") + arcs.take("n2")).unique() pos = {k: v for v, k in enumerate(nodes)} arc_dict = arcs.to_dict(result_col="w", indices=["n1", "n2"], is_list=False).to_dictdict() arc_weights = [[]] * len(nodes) for n1, n2dict in arc_dict.items(): n1list = arc_weights[pos[n1]] = [0] * len(n2dict) for n2, w in n2dict.items(): n1list[pos[n2]] = w if len(nodes)**2 == len(arcs): edge_weight_format = "FULL_MATRIX" elif abs(len(nodes)**2 - len(arcs) * 2) <= 2: edge_weight_format = "LOWER_DIAG_ROW" else: # TODO: can there another possibility? edge_weight_format = "LOWER_DIAG_ROW" dict_data = dict( name="TSP", type="TSP", comment="", dimension=len(nodes), edge_weight_type="EXPLICIT", edge_weight_format=edge_weight_format, edge_weights=arc_weights, ) return tsp.models.StandardProblem(**dict_data)
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
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