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 calculate_metrics(self) -> Dict[str, Any]: """Method to calculate the metrics of a courier""" courier_delivery_earnings = len( self.fulfilled_orders) * settings.COURIER_EARNINGS_PER_ORDER shift_duration = time_diff(self.off_time, self.on_time) if shift_duration > 0: courier_utilization = self.utilization_time / shift_duration courier_orders_delivered_per_hour = len( self.fulfilled_orders) / sec_to_hour(shift_duration) courier_bundles_picked_per_hour = len( self.accepted_notifications) / sec_to_hour(shift_duration) else: courier_utilization, courier_orders_delivered_per_hour, courier_bundles_picked_per_hour = 0, 0, 0 return { 'courier_id': self.courier_id, 'on_time': self.on_time, 'off_time': self.off_time, 'fulfilled_orders': len(self.fulfilled_orders), 'earnings': self.earnings, 'utilization_time': self.utilization_time, 'accepted_notifications': len(self.accepted_notifications), 'guaranteed_compensation': self.guaranteed_compensation, 'courier_utilization': courier_utilization, 'courier_delivery_earnings': courier_delivery_earnings, 'courier_compensation': self.earnings, 'courier_orders_delivered_per_hour': courier_orders_delivered_per_hour, 'courier_bundles_picked_per_hour': courier_bundles_picked_per_hour }
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_calculate_earnings(self): """Test to verify the mechanics of calculating the shift's earnings""" # Constants random.seed(523) on_time = time(0, 0, 0) off_time = time(2, 0, 0) # Services env = Environment() # Creates a two hour - shift courier courier = Courier(env=env, on_time=on_time, off_time=off_time) # Verifies for two scenarios how the earnings are calculated. # In the first test, raw earnings from orders are chosen. # In the second test, the hourly earnings rate is chosen. # Test 1. Creates courier earnings to select the raw earnings from orders. # Asserts that these earnings are selected over the hourly earnings rate courier.fulfilled_orders = [Order()] * 7 courier.earnings = courier._calculate_earnings() self.assertEqual( courier.earnings, len(courier.fulfilled_orders) * settings.COURIER_EARNINGS_PER_ORDER) # Test 2. Creates courier earnings to select the hourly earnings rate. # Asserts that these earnings are selected over the order earnings courier.fulfilled_orders = [Order()] * 2 courier.earnings = courier._calculate_earnings() self.assertEqual( courier.earnings, sec_to_hour(time_diff(courier.off_time, courier.on_time)) * settings.COURIER_EARNINGS_PER_HOUR)
def _schedule_log_off_event(self): """Method that allows the courier to schedule the log off time""" log_off_event = Event(env=self.env) log_off_event.callbacks.append(self._log_off_callback) log_off_delay = time_diff(self.off_time, self.on_time) self.env.schedule(event=log_off_event, priority=NORMAL, delay=log_off_delay)
def _schedule_buffer_order_event(self, order: Order): """Method that allows the dispatcher to schedule the order buffering event""" buffering_event = Event(env=self.env) buffering_event.callbacks.append(self._buffer_order_callback) self.env.schedule(event=buffering_event, priority=NORMAL, delay=time_diff(order.preparation_time, order.placement_time))
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 calculate_metrics(self) -> Dict[str, Any]: """Method to calculate the metrics of an order""" dropped_off = bool(self.drop_off_time) if dropped_off: click_to_door_time = time_diff(self.drop_off_time, self.placement_time) click_to_taken_time = time_diff(self.acceptance_time, self.placement_time) ready_to_door_time = time_diff(self.drop_off_time, self.ready_time) ready_to_pickup_time = time_diff(self.pick_up_time, self.ready_time) in_store_to_pickup_time = time_diff(self.pick_up_time, self.in_store_time) drop_off_lateness_time = time_diff(self.drop_off_time, self.expected_drop_off_time) click_to_cancel_time = None else: click_to_door_time = None click_to_taken_time = None ready_to_door_time = None ready_to_pickup_time = None in_store_to_pickup_time = None drop_off_lateness_time = None click_to_cancel_time = time_diff(self.cancellation_time, self.preparation_time) return { 'order_id': self.order_id, 'placement_time': self.placement_time, 'preparation_time': self.preparation_time, 'acceptance_time': self.acceptance_time, 'in_store_time': self.in_store_time, 'ready_time': self.ready_time, 'pick_up_time': self.pick_up_time, 'drop_off_time': self.drop_off_time, 'expected_drop_off_time': self.expected_drop_off_time, 'cancellation_time': self.cancellation_time, 'dropped_off': dropped_off, 'click_to_door_time': click_to_door_time, 'click_to_taken_time': click_to_taken_time, 'ready_to_door_time': ready_to_door_time, 'ready_to_pick_up_time': ready_to_pickup_time, 'in_store_to_pick_up_time': in_store_to_pickup_time, 'drop_off_lateness_time': drop_off_lateness_time, 'click_to_cancel_time': click_to_cancel_time }
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 _calculate_earnings(self) -> float: """Method to calculate earnings after the shift ends""" delivery_earnings = len( self.fulfilled_orders) * settings.COURIER_EARNINGS_PER_ORDER guaranteed_earnings = sec_to_hour( time_diff(self.off_time, self.on_time)) * settings.COURIER_EARNINGS_PER_HOUR if guaranteed_earnings > delivery_earnings > 0: self.guaranteed_compensation = True earnings = guaranteed_earnings else: self.guaranteed_compensation = False earnings = delivery_earnings self._log( f'Courier {self.courier_id} received earnings of ${round(earnings, 2)} ' f'for {len(self.fulfilled_orders)} orders during the complete shift' ) return earnings
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