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)
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)
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
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)
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.' )
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'
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'
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)
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)
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' )
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)
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))
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}')
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, {})
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}' )
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
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.' )