def post(self): body = request.get_json(force=True) parser = reqparse.RequestParser() try: deliveries = list( map(lambda x: Delivery.parse_obj(x), body['deliveries'])) num_vehicles = body['num_vehicles'] config = None try: config = PlannerConfig.parse_obj( body['config']) if 'config' in body else None except Exception as e: print(f"Unable to parse config - {e}") ConfigProvider.set_current_config(config) plans: List[Plan] = self.planning_service.create_plans( deliveries=deliveries, couriers=[], min_number_of_plans=num_vehicles, previous_plans=[]) except NoSolutionException as e: abort(404, e.message) # add delivery_id return jsonify(status='success', plans=list(map(lambda x: x.dict(), plans)))
def __init__( self, car_distance_matrix, car_duration_matrix, num_plans_to_create: int, starts: List[int], ends: List[int], courier_capacities: List[int], start_utilizations: List[int], node_demands: List[int], pickup_nodes: List[int], drop_nodes: List[int], deliveries_not_started: List[Tuple[int, int]], deliveries_in_progress: List[Tuple[int, int]], node_time_windows: List[TimeWindowConstraint], start_time_windows: List[TimeWindowConstraint], time_windows: List[TimeWindowConstraint], time_windows_dict: Dict[int, List[TimeWindowConstraint]], pickup_service_time: ConfigProvider.get_config().get_service_time( DeliveryEventType.pickup), drop_service_time: ConfigProvider.get_config().get_service_time( DeliveryEventType.drop), previous_plans: List[List[int]], time_limit: int = 120, ) -> None: super().__init__() self.car_distance_matrix = car_distance_matrix self.car_duration_matrix = car_duration_matrix self.num_plans_to_create = num_plans_to_create self.starts = list(starts) self.ends = list(ends) self.courier_capacities = courier_capacities self.start_utilizations = start_utilizations self.node_demands = node_demands self.pickup_nodes = pickup_nodes self.drop_nodes = drop_nodes self.deliveries_not_started = deliveries_not_started self.deliveries_in_progress = deliveries_in_progress self.node_time_windows = node_time_windows self.start_time_windows = start_time_windows self.time_windows = time_windows self.time_windows_dict = time_windows_dict self.pickup_service_time = pickup_service_time self.drop_service_time = drop_service_time self.previous_plans = previous_plans self.time_limit = time_limit
def execute(self, deliveries: List[Delivery], courier: Courier, plan: Plan, config: PlannerConfig): config.use_previous_solution = True # enforcing the use of previous solution - otherwise the computing fails # as the VRP instance will not contain the plan ConfigProvider.set_current_config(config) time_blocks, fixed_times = self.timetable_optimizer.update_etas_in_plan( deliveries=deliveries, courier=courier, plan=plan) return { 'time_blocks': list(map(lambda x: x.dict(), time_blocks)), 'fixed_times': fixed_times }
def compute_fixed_times( plan: Plan, start_node: int, vrp_instance: VehicleRoutingProblemInstance, vrp_mapping: VehicleRoutingProblemMapping) -> List[Optional[int]]: config = ConfigProvider.get_config() nodes = [start_node] + \ [vrp_mapping.pickup_to_node[event.delivery_order_ids[0]] if event.type == DeliveryEventType.pickup else vrp_mapping.drop_to_node[event.delivery_order_ids[0]] for event in plan.delivery_events] if plan.assigned_courier_id is None: return [None] * (len(nodes) - 1) fixed_times = [] for from_node, to_node, event in zip(nodes[:-1], nodes[1:], plan.delivery_events): travel_time = vrp_instance.car_duration_matrix[from_node][to_node] buffer_time = config.fixed_time_buffer service_time = vrp_instance.pickup_service_time if event.type == DeliveryEventType.pickup else vrp_instance.drop_service_time event_time = event.event_time.to_time if event.type == DeliveryEventType.pickup else event.event_time.from_time fixed_time = int(event_time - service_time - travel_time - buffer_time) fixed_times.append(fixed_time) return fixed_times
def vehicle_cost_callback(from_index, to_index): from_node = manager.IndexToNode(from_index) to_node = manager.IndexToNode(to_index) time = data.car_duration_matrix[from_node][ to_node] # in seconds is_pickup = from_node in data.pickup_nodes is_drop = from_node in data.drop_nodes waiting_time = 0 if is_pickup: waiting_time = ConfigProvider.get_config( ).pickup_waiting_time elif is_drop: waiting_time = ConfigProvider.get_config( ).drop_waiting_time return time + waiting_time
def _create_courier_capacities(cls, couriers: List[Courier], num_vehicles: int): config = ConfigProvider.get_config() capacities = [c.capacity for c in couriers] + \ [config.default_courier_capacity for _ in range(num_vehicles - len(couriers))] start_utilizations = [c.start_utilization for c in couriers] + \ [0 for _ in range(num_vehicles - len(couriers))] return capacities, start_utilizations
def _get_planner(self) -> AbstractPlanner: config = ConfigProvider.get_config() planner_type = config.planner_type if planner_type == PlannerType.or_tools: return ORToolsPlanner(routing=self.routing) elif planner_type == PlannerType.insertion_heuristic: return InsertionHeuristicsPlanner(routing=self.routing) elif planner_type == PlannerType.or_tools_insertion: return InsertionHeuristicORToolsPlanner(routing=self.routing) elif planner_type == PlannerType.go_or_tools_insertion: return InsertionHeuristicORToolsPlanner(routing=self.routing) else: return ORToolsPlanner(routing=self.routing)
def compute_optimal_timetable(self, drop_nodes: List[int], pickup_nodes: List[int], car_duration_matrix, time_windows: typing.Dict[int, List[TimeWindowConstraint]], route: List[int]) -> (List[int], List[int], float): config = ConfigProvider.get_config() plan_len = len(route) A, b = [], [] timestamps = [] time_windows_len = 0 for p in route: tws = time_windows.get(p) if tws: time_windows_len = time_windows_len + len(tws) for tw in filter(lambda x: x.from_time > 0, tws): timestamps.append(tw.from_time) timestamp_shift = min(timestamps) row_length = 2 * plan_len + time_windows_len penalty_index = 0 for idx, p in enumerate(route): eta_column = idx etd_column = plan_len + idx if p in drop_nodes: # departure time is grater than arrival time plus waiting time # eta - etd <= -waiting row = np.zeros(row_length) row[eta_column], row[etd_column] = 1, -1 A.append(row) b.append(- config.get_service_time(DeliveryEventType.drop)) if not config.allow_wait_on_drop: row = np.zeros(row_length) row[eta_column], row[etd_column] = -1, 1 A.append(row) b.append(config.get_service_time(DeliveryEventType.drop)) if p in pickup_nodes: # departure time is grater than arrival time plus waiting time # eta - etd <= -waiting row = np.zeros(row_length) row[eta_column], row[etd_column] = 1, -1 A.append(row) b.append(- config.get_service_time(DeliveryEventType.pickup)) if idx > 0: # Arrival time on current node is equal to departure time from previous plus travel time row = np.zeros(row_length) row[eta_column], row[etd_column - 1] = 1, -1 A.append(row) tt = car_duration_matrix[route[idx - 1]][p] b.append(tt) row = np.zeros(row_length) row[eta_column], row[etd_column - 1] = -1, +1 A.append(row) b.append(- tt) tws = time_windows.get(p, []) for tw in tws: tw_start = tw.from_time - timestamp_shift tw_end = tw.to_time - timestamp_shift penalty_column = 2 * plan_len + penalty_index if tw.is_hard: if tw.from_time > 0: row = np.zeros(row_length) row[etd_column] = -1 A.append(row) b.append(- tw_start) if tw.to_time < MAX_TIMESTAMP_VALUE: row = np.zeros(row_length) row[eta_column] = 1 A.append(row) b.append(tw_end) else: if tw.from_time > 0: row = np.zeros(row_length) row[etd_column], row[penalty_column] = - tw.weight, -1 A.append(row) b.append(- tw.weight * tw_start) if tw.to_time < MAX_TIMESTAMP_VALUE: row = np.zeros(row_length) row[eta_column], row[penalty_column] = tw.weight, -1 A.append(row) b.append(tw.weight * tw_end) penalty_index = penalty_index + 1 A = np.vstack(tuple(A)) b = np.array(b, dtype=int) c = np.array([0 if i < 2 * plan_len else 1 for i in range(row_length)], dtype=int) all_positive = [(0, None) for x in range(2 * plan_len)] + \ [(0, None) for x in range(time_windows_len)] # noinspection PyTypeChecker res = linprog(c, A_ub=A, b_ub=b, bounds=all_positive, method='revised simplex', options={'presolve': True, 'tol': 0.00001}) if res.success: penalty = round(res.fun, ndigits=2) etas = list(map(lambda e: e + timestamp_shift, res.x[0:plan_len])) etds = list(map(lambda e: e + timestamp_shift, res.x[plan_len:2 * plan_len])) return etas, etds, penalty raise PlanUnfeasibleException(res.message)
def config(self) -> PlannerConfig: return ConfigProvider.get_config()
def _create_time_windows(deliveries: List[Delivery], couriers: List[Courier], n: int, pickup_to_node: dict, drop_to_node: dict): config = ConfigProvider.get_config() start_time_windows = [] now_timestamp = TimestampHelper.current_timestamp() for node_idx in range(n): from_time = couriers[ node_idx].start_timelocation.time if node_idx < len( couriers) else now_timestamp start_time_windows.append( TimeWindowConstraint(node=node_idx, from_time=from_time, to_time=from_time, is_hard=True)) def create_constraint_from_specification( delivery: Delivery, specification: PenaltySpecification ) -> Optional[TimeWindowConstraint]: if specification.node_type == DeliveryEventType.pickup and delivery.pickup_time is None: return None time_block = delivery.pickup_time if specification.node_type == DeliveryEventType.pickup else delivery.delivery_time if specification.node_type == DeliveryEventType.pickup: node = pickup_to_node[delivery.id] else: node = drop_to_node[delivery.id] ret = TimeWindowConstraint(node=node, is_hard=specification.is_hard, weight=specification.weight) if specification.direction == PenaltyDirection.earliness: ret.from_time = time_block.from_time - specification.offset elif specification.direction == PenaltyDirection.lateness: if time_block.anytime: return None elif time_block.asap or time_block.to_time is not None: if time_block.asap: constrain_begin = delivery.delivery_time.from_time + config.get_asap_tolerance( specification.node_type) else: constrain_begin = delivery.delivery_time.to_time ret.to_time = constrain_begin + specification.offset return ret node_time_windows = [] for delivery in deliveries: for specification in config.penalties: tw = create_constraint_from_specification( delivery=delivery, specification=specification) if tw: node_time_windows.append(tw) time_windows = defaultdict(lambda: list()) for tw in node_time_windows + start_time_windows: time_windows[tw.node].append(tw) return node_time_windows, start_time_windows, time_windows
def _create_duration_and_distance_matrix(self, deliveries: List[Delivery], couriers: List[Courier], num_plans: int): pickup_locations = list( map(lambda x: x.origin, filter(lambda x: x.origin is not None, deliveries))) drop_locations = list(map(lambda x: x.destination, deliveries)) courier_locations = list( map(lambda x: x.start_timelocation.location, couriers)) locations = pickup_locations + drop_locations + courier_locations config = ConfigProvider.get_config() if config.return_to_hub and config.hub_location: locations.append(config.hub_location) # TODO currently a lot of unnecessary transition is computed - distances between couriers locations and # TODO from delivery location to courier locations are not necessary - should not be computed and set to INT.max car_durations, car_distances = self.routing.create_duration_distance_matrix( locations, mode=OSRMProfile.car) final_matrix_dim = len(pickup_locations) + len( drop_locations) + num_plans * 2 first_start_column_idx = len(pickup_locations) + len(drop_locations) first_end_column_idx = first_start_column_idx + num_plans def extend_matrix_by_starts_ends(matrix, default_start_value: int): extended = np.zeros(shape=(final_matrix_dim, final_matrix_dim), dtype=int) extended[:len(matrix), :len(matrix)] = matrix extended[first_start_column_idx + len(couriers):first_end_column_idx, : first_end_column_idx] = default_start_value extended[first_end_column_idx:, :] = EDGE_FORBIDDEN if config.return_to_hub: to_hub_distances = extended[:, first_start_column_idx + len(couriers)] for i in range(num_plans): extended[:, first_end_column_idx + i] = to_hub_distances extended[:, first_start_column_idx: first_end_column_idx] = EDGE_FORBIDDEN reshaped = np.zeros_like(extended) a = 2 * num_plans b = first_start_column_idx reshaped[:a, :a] = extended[b:, b:] reshaped[:a, a:] = extended[b:, :b] reshaped[a:, :a] = extended[:b, b:] reshaped[a:, a:] = extended[:b, :b] return reshaped.tolist() car_distances = extend_matrix_by_starts_ends( matrix=car_distances, default_start_value=config.default_first_point_arrival_distance) car_durations = extend_matrix_by_starts_ends( matrix=car_durations, default_start_value=config.default_first_point_arrival_time) start_locations = [i for i in range(num_plans)] end_locations = [num_plans + i for i in range(num_plans)] return car_durations, car_distances, start_locations, end_locations
def create_instance(self, deliveries: List[Delivery], couriers: List[Courier], num_plans_to_create: int, previous_plans: List[Plan]) \ -> (VehicleRoutingProblemInstance, VehicleRoutingProblemMapping): duration_matrix, distance_matrix, start_locations, end_locations\ = self._create_duration_and_distance_matrix(deliveries, couriers, num_plans_to_create) node_to_pickup, node_to_drop, pickup_to_node, drop_to_node = \ self._create_node_delivery_mappings(deliveries, num_plans_to_create) node_time_windows, start_time_windows, time_windows = \ self._create_time_windows(deliveries, couriers, num_plans_to_create, pickup_to_node, drop_to_node) deliveries_not_started, deliveries_in_progress = \ self._create_info_deliveries(deliveries=deliveries, couriers=couriers, pickup_to_node=pickup_to_node, drop_to_node=drop_to_node) previous_routes, delivery_plan_ids = \ self._create_routes_from_plans(previous_plans=previous_plans, couriers=couriers, num_plans_to_create=num_plans_to_create, pickup_to_node=pickup_to_node, drop_to_node=drop_to_node) veh_id_to_courier_id = { idx: courier.id for idx, courier in enumerate(couriers) } courier_capacities, start_utilizations = self._create_courier_capacities( couriers=couriers, num_vehicles=num_plans_to_create) num_of_nodes = len(duration_matrix) node_demands = self._create_node_demands(deliveries, pickup_to_node, drop_to_node, num_of_nodes) return VehicleRoutingProblemInstance( car_distance_matrix=distance_matrix, car_duration_matrix=duration_matrix, num_plans_to_create=num_plans_to_create, starts=start_locations, ends=end_locations, courier_capacities=courier_capacities if ConfigProvider.get_config().use_courier_capacity else None, start_utilizations=start_utilizations if ConfigProvider.get_config().use_courier_capacity else None, node_demands=node_demands if ConfigProvider.get_config().use_courier_capacity else None, deliveries_not_started=deliveries_not_started, deliveries_in_progress=deliveries_in_progress, node_time_windows=node_time_windows, start_time_windows=start_time_windows, pickup_nodes=[ pickup_to_node[key] for key in pickup_to_node.keys() ], drop_nodes=[drop_to_node[key] for key in drop_to_node.keys()], time_windows_dict=time_windows, time_windows=node_time_windows + start_time_windows, pickup_service_time=ConfigProvider.get_config().get_service_time( DeliveryEventType.pickup), drop_service_time=ConfigProvider.get_config().get_service_time( DeliveryEventType.drop), previous_plans=previous_routes if ConfigProvider.get_config().use_previous_solution else None, ), VehicleRoutingProblemMapping( plan_idx_to_courier_id=veh_id_to_courier_id, pickup_to_node=pickup_to_node, drop_to_node=drop_to_node, node_to_pickup=node_to_pickup, node_to_drop=node_to_drop, delivery_plan_ids=delivery_plan_ids)