예제 #1
0
    def _picking_up_state(self, orders: Dict[int, Order]):
        """State that simulates a courier picking up stuff at the pick up location"""

        self.condition = 'picking_up'

        self._log(f'Courier {self.courier_id} begins pick up state')

        state_start = sec_to_time(self.env.now)

        try:
            self.dispatcher.courier_picking_up_event(courier=self)
            self.dispatcher.orders_in_store_event(orders)

        except Interrupt:
            pass

        try:
            service_time = max(order.pick_up_service_time
                               for order in orders.values())
            latest_ready_time = max(order.ready_time
                                    for order in orders.values())
            waiting_time = time_diff(latest_ready_time,
                                     sec_to_time(self.env.now))
            yield self.env.timeout(delay=service_time + max(0, waiting_time))

        except Interrupt:
            pass

        self.utilization_time += time_diff(sec_to_time(self.env.now),
                                           state_start)

        self._log(f'Courier {self.courier_id} finishes pick up state')

        self.dispatcher.orders_picked_up_event(orders)
예제 #2
0
    def _moving_state(self, destination: Location):
        """State detailing how a courier moves to a destination"""

        self.condition = 'moving'
        state_start = sec_to_time(self.env.now)
        self.dispatcher.courier_moving_event(courier=self)
        yield self.env.process(
            self.movement_policy.execute(origin=self.location,
                                         destination=destination,
                                         env=self.env,
                                         courier=self))
        self.utilization_time += time_diff(sec_to_time(self.env.now),
                                           state_start)
예제 #3
0
    def _is_prospect(route: Route, courier: Courier, env_time: int) -> bool:
        """Method to establish if a courier and route are matching prospects"""

        _, time_to_first_stop = OSRMService.estimate_travelling_properties(
            origin=courier.location,
            destination=route.stops[0].location,
            vehicle=courier.vehicle)
        stops_time_offset = sum(
            abs(
                time_diff(time_1=sec_to_time(
                    int(env_time + time_to_first_stop +
                        stop.arrive_at[courier.vehicle])),
                          time_2=stop.calculate_latest_expected_time()))
            for stop in route.stops)
        distance_condition = (haversine(courier.location.coordinates,
                                        route.stops[0].location.coordinates) <=
                              settings.DISPATCHER_PROSPECTS_MAX_DISTANCE)
        stop_offset_condition = (
            stops_time_offset <=
            settings.DISPATCHER_PROSPECTS_MAX_STOP_OFFSET * route.num_stops
            if route.time_since_ready(env_time) <=
            settings.DISPATCHER_PROSPECTS_MAX_READY_TIME else True)
        courier_state_condition = (
            courier.condition == 'idle'
            or (courier.condition == 'picking_up'
                and route.initial_prospect == courier.courier_id))

        return distance_condition and stop_offset_condition and courier_state_condition
예제 #4
0
    def test_orders_dropped_off_event(self):
        """Test to verify the mechanics of orders being dropped off"""

        # Constants
        initial_time = hour_to_sec(14)
        on_time = time(14, 0, 0)
        off_time = time(16, 0, 0)

        # Services
        env = Environment(initial_time=initial_time)
        dispatcher = Dispatcher(env=env, matching_policy=DummyMatchingPolicy())

        # Creates an order and sends the picked up event
        order = Order(order_id=45, user=User(env=env))
        dispatcher.assigned_orders[order.order_id] = order
        courier = Courier(on_time=on_time, off_time=off_time)
        dispatcher.orders_dropped_off_event(orders={order.order_id: order}, courier=courier)
        env.run(until=initial_time + hour_to_sec(1))

        # Verify order properties are modified and it is allocated correctly
        self.assertEqual(order.state, 'dropped_off')
        self.assertEqual(order.drop_off_time, sec_to_time(initial_time))
        self.assertIn(order.order_id, dispatcher.fulfilled_orders.keys())
        self.assertEqual(dispatcher.assigned_orders, {})
        self.assertIn(order.order_id, courier.fulfilled_orders)
예제 #5
0
    def post_process(self):
        """Post process what happened in the World before calculating metrics for the Courier and the Order"""

        logging.info(
            f'Instance {self.instance} | Simulation finished at sim time = {sec_to_time(self.env.now)}.'
        )

        for courier_id, courier in self.dispatcher.idle_couriers.copy().items(
        ):
            courier.off_time = sec_to_time(self.env.now)
            courier.log_off_event()

        warm_up_time_start = time_add(settings.SIMULATE_FROM,
                                      settings.WARM_UP_TIME)

        for order_id, order in self.dispatcher.canceled_orders.copy().items():
            if order.cancellation_time < warm_up_time_start:
                del self.dispatcher.canceled_orders[order_id]

        for order_id, order in self.dispatcher.fulfilled_orders.copy().items():
            if order.drop_off_time < warm_up_time_start:
                del self.dispatcher.fulfilled_orders[order_id]

        logging.info(
            f'Instance {self.instance} | Post processed the simulation.')
        system(
            f'say The simulation process for instance {self.instance}, '
            f'matching policy {settings.DISPATCHER_MATCHING_POLICY} has finished.'
        )
예제 #6
0
    def orders_picked_up_event(self, orders: Dict[int, Order]):
        """Event detailing how the dispatcher handles a courier picking up an order"""

        self._log(
            f'Dispatcher will set these orders to be picked up: {list(orders.keys())}'
        )

        for order_id, order in orders.items():
            order.pick_up_time = sec_to_time(self.env.now)
            order.state = 'picked_up'
예제 #7
0
    def orders_in_store_event(self, orders: Dict[int, Order]):
        """Event detailing how the dispatcher handles a courier arriving to the store"""

        self._log(
            f'Dispatcher will set these orders to be in store: {list(orders.keys())}'
        )

        for order_id, order in orders.items():
            order.in_store_time = sec_to_time(self.env.now)
            order.state = 'in_store'
예제 #8
0
    def _dropping_off_state(self, orders: Dict[int, Order]):
        """State that simulates a courier dropping off stuff at the drop off location"""

        self.condition = 'dropping_off'

        self._log(
            f'Courier {self.courier_id} begins drop off state of orders {list(orders.keys())}'
        )

        state_start = sec_to_time(self.env.now)
        self.dispatcher.courier_dropping_off_event(courier=self)
        service_time = max(order.drop_off_service_time
                           for order in orders.values())
        yield self.env.timeout(delay=service_time)
        self.utilization_time += time_diff(sec_to_time(self.env.now),
                                           state_start)

        self._log(
            f'Courier {self.courier_id} finishes drop off state of orders {list(orders.keys())}'
        )

        self.dispatcher.orders_dropped_off_event(orders=orders, courier=self)
예제 #9
0
    def _simulate(self):
        """
        State that simulates the ongoing World of the simulated environment.
        Each second the World checks the DDBB to see which couriers log on and which users place orders.
        A general log shows the ongoing simulation progress
        """

        while True:
            orders_info = self._new_orders_info(
                current_time=sec_to_time(self.env.now))
            if orders_info is not None:
                self._new_users_procedure(orders_info)

            couriers_info = self._new_couriers_info(
                current_time=sec_to_time(self.env.now))
            if couriers_info is not None:
                self._new_couriers_procedure(couriers_info)

            logging.info(
                f'Instance {self.instance} | sim time = {sec_to_time(self.env.now)} '
                f'{world_log(self.dispatcher)}')

            yield self.env.timeout(delay=1)
예제 #10
0
    def _buffer_order_event(self):
        """Event detailing how the dispatcher buffers an order"""

        orders_for_buffering = [
            order for order_id, (scheduled_time,
                                 order) in self.placed_orders.items()
            if sec_to_time(self.env.now) >= scheduled_time
        ]

        for order in orders_for_buffering:
            del self.placed_orders[order.order_id]
            self.unassigned_orders[order.order_id] = order

            self._log(
                f'Dispatcher has moved the order {order.order_id} to the unassigned buffer'
            )
예제 #11
0
    def orders_dropped_off_event(self, orders: Dict[int, Order],
                                 courier: Courier):
        """Event detailing how the dispatcher handles the fulfillment of an order"""

        self._log(
            f'Dispatcher will set these orders to be dropped off: {list(orders.keys())}; '
            f'by courier {courier.courier_id}')

        for order_id, order in orders.items():
            if order_id not in self.fulfilled_orders.keys(
            ) and order_id in self.assigned_orders.keys():
                del self.assigned_orders[order_id]
                order.drop_off_time = sec_to_time(self.env.now)
                order.state = 'dropped_off'
                self.fulfilled_orders[order_id] = order
                courier.fulfilled_orders.append(order_id)
                order.user.order_dropped_off_event(order_id)
예제 #12
0
    def test_orders_picked_up_event(self):
        """Test to verify the mechanics of orders being picked up"""

        # Constants
        initial_time = hour_to_sec(14)

        # Services
        env = Environment(initial_time=initial_time)
        dispatcher = Dispatcher(env=env, matching_policy=DummyMatchingPolicy())

        # Creates an order and sends the picked up event
        order = Order(order_id=45)
        dispatcher.orders_picked_up_event(orders={order.order_id: order})
        env.run(until=initial_time + hour_to_sec(1))

        # Verify order properties are modified
        self.assertEqual(order.state, 'picked_up')
        self.assertEqual(order.pick_up_time, sec_to_time(initial_time))
예제 #13
0
    def cancel_order_event(self, order: Order):
        """Event detailing how the dispatcher handles a user canceling an order"""

        if (order.state != 'canceled'
                and (order.order_id in self.placed_orders.keys()
                     or order.order_id in self.unassigned_orders.keys())
                and order.order_id not in self.canceled_orders.keys()
                and order.order_id not in self.assigned_orders.keys()
                and order.order_id not in self.fulfilled_orders.keys()):
            if order.order_id in self.placed_orders.keys():
                del self.placed_orders[order.order_id]

            if order.order_id in self.unassigned_orders.keys():
                del self.unassigned_orders[order.order_id]

            order.cancellation_time = sec_to_time(self.env.now)
            order.state = 'canceled'
            order.user.condition = 'canceled'
            self.canceled_orders[order.order_id] = order

        self._log(f'Dispatcher canceled the order {order.order_id}')
예제 #14
0
    def test_notification_accepted_event(self):
        """Test to verify the mechanics of a notification being accepted by a courier"""

        # Constants
        initial_time = hour_to_sec(14)
        on_time = time(14, 0, 0)
        off_time = time(15, 0, 0)

        # Services
        env = Environment(initial_time=initial_time)
        dispatcher = Dispatcher(env=env, matching_policy=DummyMatchingPolicy())

        # Creates an instruction with an order, a courier and sends the accepted event
        order = Order(order_id=45)
        instruction = Route(
            stops=[
                Stop(orders={order.order_id: order}, position=0),
                Stop(orders={order.order_id: order}, position=1)
            ],
            orders={order.order_id: order}
        )
        dispatcher.unassigned_orders[order.order_id] = order
        courier = Courier(dispatcher=dispatcher, env=env, courier_id=89, on_time=on_time, off_time=off_time)
        courier.condition = 'idle'
        notification = Notification(
            courier=courier,
            instruction=instruction
        )
        dispatcher.notification_accepted_event(notification=notification, courier=courier)
        env.run(until=initial_time + min_to_sec(10))

        # Verify order and courier properties are modified and it is allocated correctly
        self.assertEqual(order.state, 'in_progress')
        self.assertEqual(order.acceptance_time, sec_to_time(initial_time))
        self.assertEqual(order.courier_id, courier.courier_id)
        self.assertIn(order.order_id, dispatcher.assigned_orders.keys())
        self.assertIsNotNone(courier.active_route)
        self.assertEqual(courier.active_route, instruction)
        self.assertEqual(dispatcher.unassigned_orders, {})
예제 #15
0
    def _evaluate_cancellation_event(self):
        """Event detailing how the dispatcher evaluates if it should cancel an order"""

        orders_for_evaluation = [
            order for order_id, (scheduled_time, order) in
            self.scheduled_cancellation_evaluation_orders.items()
            if sec_to_time(self.env.now) >= scheduled_time
        ]

        for order in orders_for_evaluation:
            should_cancel = self.cancellation_policy.execute(
                courier_id=order.courier_id)

            if should_cancel:
                self._log(
                    f'Dispatcher decided to cancel the order {order.order_id}')
                self.cancel_order_event(order)

            else:
                self._log(
                    f'Dispatcher decided not to cancel the order {order.order_id}'
                )
예제 #16
0
    def _generate_matching_costs(routes: List[Route], couriers: List[Courier],
                                 prospects: np.ndarray,
                                 env_time: int) -> np.ndarray:
        """Method to estimate the cost of a possible match, based on the prospects"""

        costs = np.zeros(len(prospects))

        for ix, (courier_ix, route_ix) in enumerate(prospects):
            route, courier = routes[route_ix], couriers[courier_ix]
            distance_to_first_stop, time_to_first_stop = OSRMService.estimate_travelling_properties(
                origin=courier.location,
                destination=route.stops[0].location,
                vehicle=courier.vehicle)
            costs[ix] = (
                len(route.orders) /
                (time_to_first_stop + route.time[courier.vehicle]) - time_diff(
                    time_1=sec_to_time(
                        int(env_time + time_to_first_stop +
                            route.stops[0].arrive_at[courier.vehicle])),
                    time_2=max(order.ready_time
                               for order in route.stops[0].orders.values())) *
                settings.DISPATCHER_DELAY_PENALTY)

        return costs
예제 #17
0
    def notification_accepted_event(self, notification: Notification,
                                    courier: Courier):
        """Event detailing how the dispatcher handles the acceptance of a notification by a courier"""

        self._log(
            f'Dispatcher will handle acceptance of a {notification.type.label} notification '
            f'from courier {courier.courier_id} (condition = {courier.condition})'
        )

        if notification.type == NotificationType.PREPOSITIONING:
            courier.active_route = notification.instruction

        elif notification.type == NotificationType.PICK_UP_DROP_OFF:
            order_ids = (list(notification.instruction.orders.keys())
                         if isinstance(notification.instruction, Route) else [
                             order_id for stop in notification.instruction
                             for order_id in stop.orders.keys()
                         ])
            processed_order_ids = [
                order_id for order_id in order_ids
                if (order_id in self.canceled_orders.keys()
                    or order_id in self.assigned_orders.keys()
                    or order_id in self.fulfilled_orders.keys())
            ]

            if bool(processed_order_ids):
                self._log(
                    f'Dispatcher will update the notification to courier {courier.courier_id} '
                    f'based on these orders being already processed: {processed_order_ids}'
                )
                notification.update(processed_order_ids)

            if ((isinstance(notification.instruction, Route)
                 and bool(notification.instruction.orders)
                 and bool(notification.instruction.stops))
                    or (isinstance(notification.instruction, list)
                        and bool(notification.instruction)
                        and bool(notification.instruction[0].orders))):
                order_ids = (list(notification.instruction.orders.keys()) if
                             isinstance(notification.instruction, Route) else [
                                 order_id for stop in notification.instruction
                                 for order_id in stop.orders.keys()
                             ])
                self._log(
                    f'Dispatcher will handle acceptance of orders {order_ids} '
                    f'from courier {courier.courier_id} (condition = {courier.condition}). '
                    f'Instruction is a {"Route" if isinstance(notification.instruction, Route) else "List[Stop]"}'
                )

                instruction_orders = (
                    notification.instruction.orders.items() if isinstance(
                        notification.instruction, Route) else [
                            (order_id, order)
                            for stop in notification.instruction
                            for order_id, order in stop.orders.items()
                        ])
                for order_id, order in instruction_orders:
                    del self.unassigned_orders[order_id]
                    order.acceptance_time = sec_to_time(self.env.now)
                    order.state = 'in_progress'
                    order.courier_id = courier.courier_id
                    self.assigned_orders[order_id] = order

                if courier.condition == 'idle' and isinstance(
                        notification.instruction, Route):
                    courier.active_route = notification.instruction

                elif courier.condition == 'picking_up' and isinstance(
                        notification.instruction, list):
                    for stop in notification.instruction:
                        for order_id, order in stop.orders.items():
                            courier.active_route.orders[order_id] = order
                            courier.active_stop.orders[order_id] = order

                        courier.active_route.stops.append(
                            Stop(location=stop.location,
                                 position=len(courier.active_route.stops),
                                 orders=stop.orders,
                                 type=stop.type))

                courier.accepted_notifications.append(notification)

            else:
                self._log(
                    f'Dispatcher will nullify notification to courier {courier.courier_id}. All orders canceled.'
                )