示例#1
0
    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)))
示例#2
0
    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
        }
示例#4
0
    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
示例#5
0
            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
示例#6
0
    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
示例#7
0
    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)
示例#9
0
 def config(self) -> PlannerConfig:
     return ConfigProvider.get_config()
示例#10
0
    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
示例#11
0
    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
示例#12
0
    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)