def test_prepositioning_notification_rejected_event(self): """Test to verify the mechanics of a prepositioning notification being rejected 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 a prepositioning notification, a courier and sends the rejected event instruction = Route( stops=[ Stop(position=0, type=StopType.PREPOSITION), Stop(position=1, type=StopType.PREPOSITION) ] ) courier = Courier(dispatcher=dispatcher, env=env, courier_id=981, on_time=on_time, off_time=off_time) notification = Notification(courier=courier, instruction=instruction, type=NotificationType.PREPOSITIONING) dispatcher.notification_rejected_event(notification=notification, courier=courier) env.run(until=initial_time + min_to_sec(30)) # Verify order and courier properties are modified and it is allocated correctly self.assertIsNone(courier.active_route)
def estimate_route_properties(cls, origin: Location, route: Route, vehicle: Vehicle) -> Tuple[float, float]: """Method to estimate the distance and time it would take to fulfill a route from an origin""" complete_route = Route( stops=[ Stop(location=origin, position=0) ] + [ Stop(location=stop.location, position=ix + 1) for ix, stop in enumerate(route.stops) ] ) route_distance, route_time = 0, 0 try: for ix in range(len(complete_route.stops) - 1): distance, time = cls.estimate_travelling_properties( origin=complete_route.stops[ix].location, destination=complete_route.stops[ix + 1].location, vehicle=vehicle ) route_distance += distance route_time += time except: logging.exception('Exception captured in OSRMService.estimate_route_properties. Check Docker.') return route_distance, route_time
def _get_estimations(orders: List[Order], couriers: List[Courier], prospects: np.ndarray) -> np.ndarray: """Method to obtain the time estimations from the matching prospects""" estimations = [None] * len(prospects) for ix, (order_ix, courier_ix) in enumerate(prospects): order, courier = orders[order_ix], couriers[courier_ix] route = Route(orders={order.order_id: order}, stops=[ Stop(location=order.pick_up_at, orders={order.order_id: order}, position=0, type=StopType.PICK_UP, visited=False), Stop(location=order.drop_off_at, orders={order.order_id: order}, position=1, type=StopType.DROP_OFF, visited=False) ]) distance, time = OSRMService.estimate_route_properties( origin=courier.location, route=route, vehicle=courier.vehicle) time += (order.pick_up_service_time + order.drop_off_service_time) estimations[ix] = (distance, time) return np.array(estimations, dtype=[('distance', np.float64), ('time', np.float64)])
def test_pick_up_waiting_time(self, osrm): """Test to verify the mechanics of the waiting time are correctly designed""" # Constants random.seed(290) on_time = time(6, 0, 0) off_time = time(8, 0, 0) # Services env = Environment(initial_time=hour_to_sec(6)) dispatcher = Dispatcher(env=env, matching_policy=DummyMatchingPolicy()) # Creates a courier and sets it to pick up stuff courier = Courier( acceptance_policy=self.acceptance_policy, dispatcher=dispatcher, env=env, movement_evaluation_policy=self.movement_evaluation_policy, movement_policy=self.movement_policy, courier_id=self.courier_id, vehicle=self.vehicle, location=self.start_location, acceptance_rate=0.01, on_time=on_time, off_time=off_time) order = Order(ready_time=time(6, 15, 0), order_id=23) stop = Stop(orders={order.order_id: order}, type=StopType.PICK_UP) env.process(courier._execute_stop(stop)) dispatcher.state.interrupt() # Run until there are no more events and assert the courier experienced waiting time. env.run(until=hour_to_sec(7)) self.assertTrue(order.pick_up_time >= time( 6, int(order.ready_time.minute + order.pick_up_service_time / 60))) # For another test, if the order's ready time has expired, the courier doesn't experience waiting time env = Environment(initial_time=hour_to_sec(6)) dispatcher = Dispatcher(env=env, matching_policy=DummyMatchingPolicy()) courier = Courier( acceptance_policy=self.acceptance_policy, dispatcher=dispatcher, env=env, movement_evaluation_policy=self.movement_evaluation_policy, movement_policy=self.movement_policy, courier_id=self.courier_id, vehicle=self.vehicle, location=self.start_location, acceptance_rate=0.01, on_time=on_time, off_time=off_time) order = Order(ready_time=time(4, 0, 0), order_id=23) stop = Stop(orders={order.order_id: order}, type=StopType.PICK_UP) env.process(courier._execute_stop(stop)) dispatcher.state.interrupt() env.run(until=hour_to_sec(7)) self.assertTrue( time(order.pick_up_time.hour, order.pick_up_time.minute) <= time( 6, int(order.pick_up_service_time / 60)))
def test_estimate_route_properties(self, osrm): """Test to verify the route estimation works correctly""" # Defines an origin and a route that must be fulfilled origin = Location(4.678622, -74.055694) route = Route(stops=[ Stop(position=0, location=Location(4.690207, -74.044235)), Stop(position=1, location=Location(4.709022, -74.035102)) ]) # Obtains the route's distance and time and asserts expected values distance, time = OSRMService.estimate_route_properties( origin=origin, route=route, vehicle=Vehicle.CAR) self.assertEqual(int(distance), 4) self.assertEqual(time, 594)
def test_get_route(self, osrm): """Test to verify the route construction works correctly""" # Defines an origin and a destination origin = Location(4.678622, -74.055694) destination = Location(4.690207, -74.044235) # Obtains the route and asserts it is equal to the mocked value route = OSRMService.get_route(origin, destination) self.assertEqual( route.stops, Route(stops=[ Stop(position=0, location=origin), Stop(position=1, location=destination) ]).stops)
def from_order(cls, order: Order): """Method to instantiate a route from an order""" orders = {order.order_id: order} pick_up_stop = Stop(location=order.pick_up_at, orders=orders, position=0, type=StopType.PICK_UP, visited=False) drop_off_stop = Stop(location=order.drop_off_at, orders=orders, position=1, type=StopType.DROP_OFF, visited=False) return cls(orders=orders, stops=[pick_up_stop, drop_off_stop])
def update(self, processed_order_ids: List[int]): """Method to update a notification if some of its orders have been processed""" if isinstance(self.instruction, Route): self.instruction.update(processed_order_ids) else: updated_stops, num_stops = [], 0 for stop in self.instruction: updated_orders = { order_id: order for order_id, order in stop.orders.items() if order_id not in processed_order_ids } if bool(updated_orders): updated_stops.append( Stop( arrive_at=stop.arrive_at, location=stop.location, orders=updated_orders, position=num_stops, type=stop.type, visited=stop.visited ) ) num_stops += 1 self.instruction = updated_stops
def add_stops(self, target_size: int): """Method to add empty stops to the route based on a target size""" while len(self.stops) - 1 < target_size: self.stops.append(Stop()) self.num_stops = len(self.stops)
def update(self, processed_order_ids: List[int]): """Method to update a route if some of its orders have been processed""" updated_stops, num_stops = [], 0 for stop in self.stops: updated_orders = { order_id: order for order_id, order in stop.orders.items() if order_id not in processed_order_ids } if bool(updated_orders): updated_stops.append( Stop(arrive_at=stop.arrive_at, location=stop.location, orders=updated_orders, position=num_stops, type=stop.type, visited=stop.visited)) num_stops += 1 self.stops = updated_stops self.orders = { order_id: order for order_id, order in self.orders.items() if order_id not in processed_order_ids } self.num_stops = len(self.stops) self.time = self._calculate_time()
def __post_init__(self): """Post process of the route creation""" self.stops = [Stop() ] * self.num_stops if self.num_stops else self.stops self.num_stops = len(self.stops) self.time = {v: 0 for v in Vehicle} if bool(self.orders): self.time = self._calculate_time() self.route_id = str(uuid.uuid4())[0:8]
def get_route(cls, origin: Location, destination: Location) -> Route: """Method to obtain a movement route using docker-mounted OSRM""" lat_0, lng_0 = origin.coordinates lat_1, lng_1 = destination.coordinates url = cls.URL.format(lng_0=lng_0, lat_0=lat_0, lng_1=lng_1, lat_1=lat_1) try: response = requests.get(url, timeout=5) if response and response.status_code in [requests.codes.ok, requests.codes.no_content]: response_data = response.json() steps = response_data.get('routes', [])[0].get('legs', [])[0].get('steps', []) stops = [] for ix, step in enumerate(steps): lng, lat = step.get('maneuver', {}).get('location', []) stop = Stop( location=Location(lat=lat, lng=lng), position=ix ) stops.append(stop) return Route(stops=stops) except: logging.exception('Exception captured in OSRMService.get_route. Check Docker.') return Route( stops=[ Stop( location=origin, position=0 ), Stop( location=destination, position=1 ) ] )
def estimate_travelling_properties( cls, origin: Location, destination: Location, vehicle: Vehicle ) -> Tuple[float, float]: """Method to estimate the distance and time it takes to go from an origin to a destination""" route_distance, route_time = 0, 0 try: travelling_route = cls.get_route(origin=origin, destination=destination) except: logging.exception('Exception captured in OSRMService.estimate_travelling_properties. Check Docker.') travelling_route = Route( stops=[ Stop( location=origin, position=0 ), Stop( location=destination, position=1 ) ] ) for travelling_ix in range(len(travelling_route.stops) - 1): distance = haversine( point1=travelling_route.stops[travelling_ix].location.coordinates, point2=travelling_route.stops[travelling_ix + 1].location.coordinates ) time = int(distance / vehicle.average_velocity) route_distance += distance route_time += time return route_distance, route_time
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 _execute_stop(self, stop: Stop): """State to execute a stop""" self.active_stop = stop self._log( f'Courier {self.courier_id} is at stop of type {self.active_stop.type.label} ' f'with orders {list(stop.orders.keys())}, on location {stop.location}' ) service_state = self._picking_up_state if stop.type == StopType.PICK_UP else self._dropping_off_state yield self.env.process(service_state(orders=stop.orders)) stop.visited = True
def test_notify_prepositioning_event_accept_idle(self, osrm): """Test to evaluate how a courier handles a prepositioning notification while being idle and accepts it""" # Constants random.seed(348) initial_time = hour_to_sec(17) time_delta = min_to_sec(10) on_time = time(17, 0, 0) off_time = time(17, 30, 0) # Services env = Environment(initial_time=initial_time) dispatcher = Dispatcher(env=env, matching_policy=DummyMatchingPolicy()) # Creates a courier with high acceptance rate and immediately send a prepositioning notification courier = Courier( acceptance_policy=self.acceptance_policy, dispatcher=dispatcher, env=env, movement_evaluation_policy=self.movement_evaluation_policy, movement_policy=self.movement_policy, courier_id=self.courier_id, vehicle=self.vehicle, location=self.start_location, acceptance_rate=0.99, on_time=on_time, off_time=off_time) instruction = Route(orders=None, stops=[ Stop(location=self.pick_up_at, position=0, orders=None, type=StopType.PREPOSITION, visited=False) ]) notification = Notification(courier=courier, instruction=instruction, type=NotificationType.PREPOSITIONING) env.process(courier.notification_event(notification)) env.run(until=initial_time + time_delta) # Asserts that the courier fulfilled the route and is at a different start location self.assertIsNone(courier.active_route) self.assertIsNone(courier.active_stop) self.assertEqual(dispatcher.fulfilled_orders, {}) self.assertNotEqual(courier.location, self.start_location) self.assertEqual(courier.condition, 'idle') self.assertIn(courier.courier_id, dispatcher.idle_couriers.keys())
def update_stops(self): """Method to remove empty stops from the route""" stops, counter = [], 0 for stop in self.stops: if bool(stop.orders): stops.append( Stop(arrive_at=stop.arrive_at, location=stop.location, orders=stop.orders, position=counter, type=stop.type, visited=stop.visited)) counter += 1 self.stops = stops self.num_stops = len(self.stops)
def add_order(self, order: Order, route_position: Optional[int] = 1): """Method to add an order to the route""" if not bool(self.orders): pick_up_stop = Stop(location=order.pick_up_at, orders={order.order_id: order}, position=0, type=StopType.PICK_UP, visited=False) drop_off_stop = Stop(location=order.drop_off_at, orders={order.order_id: order}, position=1, type=StopType.DROP_OFF, visited=False) self.orders[order.order_id] = order self.stops[0] = pick_up_stop self.stops[1] = drop_off_stop time = self._calculate_time() else: self.orders[order.order_id] = order self.stops[0].orders[order.order_id] = order stop = Stop(location=order.drop_off_at, orders={order.order_id: order}, position=route_position, type=StopType.DROP_OFF, visited=False) position = max(route_position, len(self.stops) - 1) if position <= len(self.stops) - 1 and not bool( self.stops[position].orders): self.stops[position] = stop else: position = len(self.stops) stop.position = position self.stops.append(stop) time = self.calculate_time_update( destination=stop.location, origin=self.stops[position - 1].location, service_time=stop.calculate_service_time()) stop.arrive_at = copy.deepcopy(time) self.time = time self.num_stops = len(self.stops)
def test_generate_matching_prospects_picking_up_couriers(self, osrm): """Test to verify how prospects are created""" # Constants env_time = hour_to_sec(12) + min_to_sec(20) on_time = time(8, 0, 0) off_time = time(16, 0, 0) # Orders order_1 = Order(order_id=1, pick_up_at=Location(lat=4.678759, lng=-74.055729), drop_off_at=Location(lat=4.681694, lng=-74.044811), ready_time=time(12, 30, 0), expected_drop_off_time=time(12, 40, 0), pick_up_service_time=0, drop_off_service_time=0) order_2 = Order(order_id=2, pick_up_at=Location(lat=4.678759, lng=-74.055729), drop_off_at=Location(lat=4.695001, lng=-74.040737), ready_time=time(12, 32, 0), expected_drop_off_time=time(12, 42, 0), pick_up_service_time=0, drop_off_service_time=0) order_3 = Order(order_id=3, pick_up_at=Location(lat=4.678759, lng=-74.055729), drop_off_at=Location(lat=4.668742, lng=-74.056684), ready_time=time(12, 33, 0), expected_drop_off_time=time(12, 43, 0), pick_up_service_time=0, drop_off_service_time=0) # Couriers courier_3 = Courier( courier_id=3, on_time=on_time, off_time=off_time, condition='picking_up', location=order_3.pick_up_at, active_route=Route(orders={order_3.order_id: order_3}, stops=[ Stop(location=order_3.pick_up_at, orders={order_3.order_id: order_3}, position=0, type=StopType.PICK_UP), Stop(location=order_3.drop_off_at, orders={order_3.order_id: order_3}, position=1, type=StopType.DROP_OFF) ]), active_stop=Stop(location=order_3.pick_up_at, orders={order_3.order_id: order_3}, position=0, type=StopType.PICK_UP)) # Get routes and assert expected behavior policy = MyopicMatchingPolicy(assignment_updates=True, prospects=True, notification_filtering=False, mip_matcher=False) routes = policy._generate_routes(orders=[order_1, order_2], couriers=[courier_3], env_time=env_time) # Generate prospects and assert expected behavior prospects = policy._generate_matching_prospects(routes=routes, couriers=[courier_3], env_time=env_time) self.assertFalse(prospects.tolist())
def execute(self, orders: List[Order], couriers: List[Courier], env_time: int) -> Tuple[List[Notification], MatchingMetric]: """Implementation of the policy""" matching_start_time = time.time() idle_couriers = [ courier for courier in couriers if courier.condition == 'idle' and courier.active_route is None ] prospects = self._get_prospects(orders, idle_couriers) estimations = self._get_estimations(orders, idle_couriers, prospects) notifications, notified_couriers = [], np.array([]) if bool(prospects.tolist()) and bool( estimations.tolist()) and bool(orders) and bool(idle_couriers): for order_ix, order in enumerate(orders): mask = np.where( np.logical_and( prospects[:, 0] == order_ix, np.logical_not( np.isin(prospects[:, 1], notified_couriers)))) if bool(mask[0].tolist()): order_prospects = prospects[mask] order_estimations = estimations[mask] min_time = order_estimations['time'].min() selection_mask = np.where( order_estimations['time'] == min_time) selected_prospect = order_prospects[selection_mask][0] notifications.append( Notification( courier=couriers[selected_prospect[1]], type=NotificationType.PICK_UP_DROP_OFF, instruction=Route( orders={order.order_id: order}, stops=[ Stop(location=order.pick_up_at, orders={order.order_id: order}, position=0, type=StopType.PICK_UP, visited=False), Stop(location=order.drop_off_at, orders={order.order_id: order}, position=1, type=StopType.DROP_OFF, visited=False) ]))) notified_couriers = np.append(notified_couriers, selected_prospect[1]) matching_time = time.time() - matching_start_time matching_metric = MatchingMetric(constraints=0, couriers=len(couriers), matches=len(notifications), matching_time=matching_time, orders=len(orders), routes=len(orders), routing_time=0., variables=0) return notifications, matching_metric
def test_add_order(self, osrm): """Test to verify how a new order is added to an existing route""" # Constants old_order = Order(order_id=5, pick_up_at=Location(lat=4.567, lng=1.234), drop_off_at=Location(lat=1.234, lng=4.567)) new_order = Order(order_id=1, pick_up_at=Location(lat=1.234, lng=4.567), drop_off_at=Location(lat=4.567, lng=1.234)) # Case 1: the route is empty route = Route(num_stops=2) route.add_order(new_order) self.assertTrue(route.stops) self.assertEqual(len(route.stops), 2) self.assertEqual(len(route.stops), route.num_stops) self.assertIn(new_order.order_id, route.orders.keys()) self.assertIn(new_order.order_id, route.stops[0].orders.keys()) self.assertIn(new_order.order_id, route.stops[1].orders.keys()) self.assertEqual(route.stops[0].type, StopType.PICK_UP) self.assertEqual(route.stops[1].type, StopType.DROP_OFF) self.assertTrue(route.time) # Case 2. the route has an order and is inserted at correct position route = Route(orders={old_order.order_id: old_order}, stops=[ Stop(location=old_order.pick_up_at, orders={old_order.order_id: old_order}, position=0, type=StopType.PICK_UP), Stop(location=old_order.drop_off_at, orders={old_order.order_id: old_order}, position=1, type=StopType.DROP_OFF) ]) route.add_order(new_order, route_position=2) self.assertTrue(route) self.assertEqual(len(route.stops), 3) self.assertEqual(len(route.stops), route.num_stops) self.assertIn(new_order.order_id, route.orders.keys()) self.assertIn(old_order.order_id, route.orders.keys()) self.assertIn(new_order.order_id, route.stops[0].orders.keys()) self.assertIn(old_order.order_id, route.stops[0].orders.keys()) self.assertIn(old_order.order_id, route.stops[1].orders.keys()) self.assertIn(new_order.order_id, route.stops[2].orders.keys()) self.assertEqual(route.stops[0].type, StopType.PICK_UP) self.assertEqual(route.stops[1].type, StopType.DROP_OFF) self.assertEqual(route.stops[2].type, StopType.DROP_OFF) self.assertTrue(route.time) # Case 3. the route has an order and is inserted at wrong position (greater position) route = Route(orders={old_order.order_id: old_order}, stops=[ Stop(location=old_order.pick_up_at, orders={old_order.order_id: old_order}, position=0, type=StopType.PICK_UP), Stop(location=old_order.drop_off_at, orders={old_order.order_id: old_order}, position=1, type=StopType.DROP_OFF) ]) route.add_order(new_order, route_position=6) self.assertTrue(route) self.assertEqual(len(route.stops), 3) self.assertEqual(len(route.stops), route.num_stops) self.assertIn(new_order.order_id, route.orders.keys()) self.assertIn(old_order.order_id, route.orders.keys()) self.assertIn(new_order.order_id, route.stops[0].orders.keys()) self.assertIn(old_order.order_id, route.stops[0].orders.keys()) self.assertIn(old_order.order_id, route.stops[1].orders.keys()) self.assertIn(new_order.order_id, route.stops[2].orders.keys()) self.assertEqual(route.stops[0].type, StopType.PICK_UP) self.assertEqual(route.stops[1].type, StopType.DROP_OFF) self.assertEqual(route.stops[2].type, StopType.DROP_OFF) self.assertTrue(route.time) # Case 4. the route has an order and is inserted at wrong position (equal position) route = Route(orders={old_order.order_id: old_order}, stops=[ Stop(location=old_order.pick_up_at, orders={old_order.order_id: old_order}, position=0, type=StopType.PICK_UP), Stop(location=old_order.drop_off_at, orders={old_order.order_id: old_order}, position=1, type=StopType.DROP_OFF) ]) route.add_order(new_order, route_position=1) self.assertTrue(route) self.assertEqual(len(route.stops), 3) self.assertEqual(len(route.stops), route.num_stops) self.assertIn(new_order.order_id, route.orders.keys()) self.assertIn(old_order.order_id, route.orders.keys()) self.assertIn(new_order.order_id, route.stops[0].orders.keys()) self.assertIn(old_order.order_id, route.stops[0].orders.keys()) self.assertIn(old_order.order_id, route.stops[1].orders.keys()) self.assertIn(new_order.order_id, route.stops[2].orders.keys()) self.assertEqual(route.stops[0].type, StopType.PICK_UP) self.assertEqual(route.stops[1].type, StopType.DROP_OFF) self.assertEqual(route.stops[2].type, StopType.DROP_OFF) self.assertTrue(route.time) # Case 5. the route has an order and is inserted at wrong position (smaller position) route = Route(orders={old_order.order_id: old_order}, stops=[ Stop(location=old_order.pick_up_at, orders={old_order.order_id: old_order}, position=0, type=StopType.PICK_UP), Stop(location=old_order.drop_off_at, orders={old_order.order_id: old_order}, position=1, type=StopType.DROP_OFF) ]) route.add_order(new_order, route_position=1) self.assertTrue(route) self.assertEqual(len(route.stops), 3) self.assertEqual(len(route.stops), route.num_stops) self.assertIn(new_order.order_id, route.orders.keys()) self.assertIn(old_order.order_id, route.orders.keys()) self.assertIn(new_order.order_id, route.stops[0].orders.keys()) self.assertIn(old_order.order_id, route.stops[0].orders.keys()) self.assertIn(old_order.order_id, route.stops[1].orders.keys()) self.assertIn(new_order.order_id, route.stops[2].orders.keys()) self.assertEqual(route.stops[0].type, StopType.PICK_UP) self.assertEqual(route.stops[1].type, StopType.DROP_OFF) self.assertEqual(route.stops[2].type, StopType.DROP_OFF) self.assertTrue(route.time)
def test_notify_event_reject_idle(self, osrm): """Test to evaluate how a courier handles a notification while being idle and rejects it""" # Constants random.seed(122) on_time = time(12, 0, 0) off_time = time(15, 0, 0) # Services env = Environment(initial_time=hour_to_sec(12)) dispatcher = Dispatcher(env=env, matching_policy=DummyMatchingPolicy()) # Creates a courier with low acceptance rate and immediately send a new instruction, composed of a single order courier = Courier( acceptance_policy=self.acceptance_policy, dispatcher=dispatcher, env=env, movement_evaluation_policy=self.movement_evaluation_policy, movement_policy=self.movement_policy, courier_id=self.courier_id, vehicle=self.vehicle, location=self.start_location, acceptance_rate=0.01, on_time=on_time, off_time=off_time) order = Order(order_id=self.order_id, drop_off_at=self.drop_off_at, pick_up_at=self.pick_up_at, placement_time=self.placement_time, expected_drop_off_time=self.expected_drop_off_time, preparation_time=self.preparation_time, ready_time=self.ready_time) dispatcher.unassigned_orders[order.order_id] = order instruction = Route(orders={self.order_id: order}, stops=[ Stop(location=self.pick_up_at, position=0, orders={self.order_id: order}, type=StopType.PICK_UP, visited=False), Stop(location=self.drop_off_at, position=1, orders={self.order_id: order}, type=StopType.DROP_OFF, visited=False) ]) notification = Notification(courier=courier, instruction=instruction) env.process(courier.notification_event(notification)) env.run(until=hour_to_sec(14)) # Asserts that the courier didn't fulfill the route self.assertIsNone(order.pick_up_time) self.assertIsNone(order.drop_off_time) self.assertIsNone(order.courier_id) self.assertIsNone(courier.active_route) self.assertIsNone(courier.active_stop) self.assertIn(courier.courier_id, order.rejected_by) self.assertIn(order.order_id, courier.rejected_orders) self.assertEqual(dispatcher.unassigned_orders, {order.order_id: order}) self.assertEqual(order.state, 'unassigned') self.assertEqual(courier.location, self.start_location) self.assertEqual(courier.condition, 'idle') self.assertIn(courier.courier_id, dispatcher.idle_couriers.keys())
def mocked_get_route(origin: Location, destination: Location) -> Route: """Method that mocks how a route is obtained going from an origin to a destination""" return Route(stops=[Stop(location=origin, position=0), Stop(location=destination, position=1)])
def test_notify_event_reject_picking_up(self, osrm): """Test to evaluate how a courier handles a notification while picking up and rejects it""" # Constants random.seed(4747474) on_time = time(12, 0, 0) off_time = time(15, 0, 0) # Services env = Environment(initial_time=hour_to_sec(12) + min_to_sec(12)) dispatcher = Dispatcher(env=env, matching_policy=DummyMatchingPolicy()) # Creates a courier with low acceptance rate, an active route and in state of picking up. # Sends a new instruction, composed of a single new order active_order = Order( order_id=self.order_id, drop_off_at=self.drop_off_at, pick_up_at=self.pick_up_at, placement_time=self.placement_time, expected_drop_off_time=self.expected_drop_off_time, preparation_time=self.preparation_time, ready_time=self.ready_time, courier_id=self.courier_id, user=User(env=env)) dispatcher.assigned_orders[active_order.order_id] = active_order new_order = Order(order_id=17, drop_off_at=Location(lat=4.694627, lng=-74.038886), pick_up_at=self.pick_up_at, placement_time=self.placement_time, expected_drop_off_time=self.expected_drop_off_time, preparation_time=self.preparation_time, ready_time=self.ready_time, user=User(env=env)) dispatcher.unassigned_orders[new_order.order_id] = new_order courier = Courier( acceptance_policy=self.acceptance_policy, dispatcher=dispatcher, env=env, movement_evaluation_policy=self.movement_evaluation_policy, movement_policy=self.movement_policy, courier_id=self.courier_id, vehicle=self.vehicle, location=active_order.pick_up_at, acceptance_rate=0.01, active_route=Route(orders={self.order_id: active_order}, stops=[ Stop(location=self.pick_up_at, position=0, orders={self.order_id: active_order}, type=StopType.PICK_UP, visited=False), Stop(location=self.drop_off_at, position=1, orders={self.order_id: active_order}, type=StopType.DROP_OFF, visited=False) ]), on_time=on_time, off_time=off_time) instruction = Stop(location=new_order.drop_off_at, position=1, orders={new_order.order_id: new_order}, type=StopType.DROP_OFF, visited=False) notification = Notification(courier=courier, instruction=instruction) courier.state.interrupt() courier.active_stop = courier.active_route.stops[0] courier.state = env.process( courier._picking_up_state( orders={active_order.order_id: active_order})) env.process(courier.notification_event(notification)) env.run(until=hour_to_sec(14)) # Asserts: # - the courier didn't fulfill the new order, # - fulfilled the active order and # - is at a different start location. self.assertIsNone(new_order.pick_up_time) self.assertIsNone(new_order.drop_off_time) self.assertIsNone(new_order.courier_id) self.assertIn(courier.courier_id, new_order.rejected_by) self.assertIn(new_order.order_id, courier.rejected_orders) self.assertEqual(new_order.state, 'unassigned') self.assertIsNotNone(active_order.pick_up_time) self.assertIsNotNone(active_order.drop_off_time) self.assertEqual(active_order.courier_id, courier.courier_id) self.assertTrue(active_order.pick_up_time < active_order.drop_off_time) self.assertEqual(active_order.state, 'dropped_off') self.assertIsNone(courier.active_route) self.assertIsNone(courier.active_stop) self.assertNotEqual(courier.location, self.start_location) self.assertEqual(dispatcher.fulfilled_orders, {active_order.order_id: active_order}) self.assertEqual(dispatcher.unassigned_orders, {new_order.order_id: new_order}) self.assertEqual(courier.condition, 'idle') self.assertIn(courier.courier_id, dispatcher.idle_couriers.keys())
def test_update_route(self, osrm): """Test to verify a route is updated based on canceled orders""" # Constants order_1 = Order(order_id=1, pick_up_at=Location(lat=4.567, lng=1.234), drop_off_at=Location(lat=1.234, lng=4.567)) order_2 = Order(order_id=2, pick_up_at=Location(lat=4.567, lng=1.234), drop_off_at=Location(lat=1.234, lng=4.567)) # Test 1: define a route and some orders being canceled orders_dict = {order_1.order_id: order_1, order_2.order_id: order_2} route = Route(orders=orders_dict, stops=[ Stop(orders={order_1.order_id: order_1}, type=StopType.PICK_UP, position=0, location=order_1.pick_up_at), Stop(orders={order_2.order_id: order_2}, type=StopType.PICK_UP, position=1, location=order_2.pick_up_at), Stop(orders={order_1.order_id: order_1}, type=StopType.DROP_OFF, position=2, location=order_1.drop_off_at), Stop(orders={order_2.order_id: order_2}, type=StopType.DROP_OFF, position=3, location=order_2.drop_off_at) ]) canceled_order_ids = [1] # Update the route and assert canceled orders were removed route.update(canceled_order_ids) self.assertEqual(len(route.orders), 1) self.assertEqual(len(route.stops), 2) for stop in route.stops: self.assertNotIn(order_1.order_id, stop.orders) self.assertEqual(len(stop.orders), 1) # Test 2: define a route and all orders being canceled orders_dict = {order_1.order_id: order_1, order_2.order_id: order_2} route = Route(orders=orders_dict, stops=[ Stop(orders=orders_dict, type=StopType.PICK_UP, position=0, location=order_1.pick_up_at), Stop(orders=orders_dict, type=StopType.DROP_OFF, position=2, location=order_1.drop_off_at) ]) canceled_order_ids = [1, 2] # Update the route and assert canceled orders were removed route.update(canceled_order_ids) self.assertEqual(len(route.orders), 0) self.assertEqual(len(route.stops), 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.' )
def test_myopic_matching_policy_execute_mip_matcher(self, osrm): """Test to verify how the optimization model is solved with a MIP approach""" # Constants env_time = hour_to_sec(12) + min_to_sec(20) on_time = time(8, 0, 0) off_time = time(16, 0, 0) random.seed(45) # Orders order_1 = Order(order_id=1, pick_up_at=Location(lat=4.678759, lng=-74.055729), drop_off_at=Location(lat=4.681694, lng=-74.044811), ready_time=time(12, 30, 0), expected_drop_off_time=time(12, 40, 0), pick_up_service_time=0, drop_off_service_time=0) order_2 = Order(order_id=2, pick_up_at=Location(lat=4.678759, lng=-74.055729), drop_off_at=Location(lat=4.695001, lng=-74.040737), ready_time=time(12, 32, 0), expected_drop_off_time=time(12, 42, 0), pick_up_service_time=0, drop_off_service_time=0) order_3 = Order(order_id=3, pick_up_at=Location(lat=4.678759, lng=-74.055729), drop_off_at=Location(lat=4.668742, lng=-74.056684), ready_time=time(12, 33, 0), expected_drop_off_time=time(12, 43, 0), pick_up_service_time=0, drop_off_service_time=0) order_4 = Order(order_id=4, pick_up_at=Location(lat=4.678759, lng=-74.055729), drop_off_at=Location(lat=4.661441, lng=-74.056955), ready_time=time(12, 34, 0), expected_drop_off_time=time(12, 44, 0), pick_up_service_time=0, drop_off_service_time=0) # Couriers courier_1 = Courier(courier_id=1, on_time=on_time, off_time=off_time, condition='idle', location=Location(lat=4.676854, lng=-74.057498)) courier_2 = Courier(courier_id=2, on_time=on_time, off_time=off_time, condition='idle', location=Location(lat=4.679408, lng=-74.052524)) courier_3 = Courier( courier_id=3, on_time=on_time, off_time=off_time, condition='picking_up', location=order_3.pick_up_at, active_route=Route(orders={order_3.order_id: order_3}, stops=[ Stop(location=order_3.pick_up_at, orders={order_3.order_id: order_3}, position=0, type=StopType.PICK_UP), Stop(location=order_3.drop_off_at, orders={order_3.order_id: order_3}, position=1, type=StopType.DROP_OFF) ]), active_stop=Stop(location=order_3.pick_up_at, orders={order_3.order_id: order_3}, position=0, type=StopType.PICK_UP)) # Get all the elements from the policy and assert their expected behavior policy = MyopicMatchingPolicy(assignment_updates=False, prospects=False, notification_filtering=False, mip_matcher=False) routes = policy._generate_routes( orders=[order_1, order_2, order_4], couriers=[courier_1, courier_2, courier_3], env_time=env_time) self.assertTrue(routes) self.assertEqual(len(routes), 2) self.assertEqual(len(routes[0].orders), 2) self.assertEqual(len(routes[1].orders), 1) prospects = policy._generate_matching_prospects( routes=routes, couriers=[courier_1, courier_2, courier_3], env_time=env_time) self.assertTrue(prospects.tolist()) self.assertEqual(len(prospects), 4), self.assertEqual(len(prospects), len(routes) * len([courier_1, courier_2])) costs = policy._generate_matching_costs( routes=routes, couriers=[courier_1, courier_2, courier_3], prospects=prospects, env_time=env_time) self.assertTrue(costs.tolist()) self.assertEqual(len(prospects), len(costs)) self.assertEqual(len(costs), 4) self.assertNotIn(0., costs) problem = MatchingProblemBuilder.build( routes=routes, couriers=[courier_1, courier_2, courier_3], prospects=prospects, costs=costs) self.assertTrue(problem) self.assertEqual(len(prospects), len(problem.prospects)) self.assertEqual(len(prospects), len(problem.matching_prospects)) self.assertEqual(len(prospects), len(problem.costs)) self.assertEqual(routes, problem.routes) self.assertEqual(problem.couriers, [courier_1, courier_2, courier_3]) self.assertNotIn(str(courier_3.courier_id), problem.matching_prospects['i']) model_builder = MIPOptimizationModelBuilder( sense='max', model_constraints=[ CourierAssignmentConstraint(), RouteAssignmentConstraint() ], optimizer='pulp') model = model_builder.build(problem) self.assertTrue(model) self.assertEqual(len(model.constraints), len(problem.routes) + len([courier_1, courier_2])) self.assertEqual(len(model.variable_set), len(problem.matching_prospects) + len(problem.routes)) solution = model.solve() self.assertTrue(solution.tolist()) self.assertEqual(len(solution), len(problem.matching_prospects) + len(problem.routes)) self.assertEqual(solution[0:len(problem.prospects)].sum(), 2) self.assertEqual(solution.sum(), 2) notifications = policy._process_solution(solution, problem, env_time) self.assertEqual(len(notifications), len(routes)) self.assertIsInstance(notifications[0].instruction, Route) self.assertIsInstance(notifications[1].instruction, Route) self.assertEqual(notifications[0].courier, courier_1) self.assertEqual(notifications[1].courier, courier_2) self.assertIn(order_1.order_id, notifications[1].instruction.orders.keys()) self.assertIn(order_4.order_id, notifications[1].instruction.orders.keys())
def _process_solution(self, solution: np.ndarray, matching_problem: MatchingProblem, env_time: int) -> List[Notification]: """Method to parse the optimizer solution into the notifications""" matching_solution = solution[0:len(matching_problem.prospects)] matched_prospects_ix = np.where(matching_solution >= SOLUTION_VALUE) matched_prospects = matching_problem.prospects[matched_prospects_ix] if not self._notification_filtering: notifications = [None] * len(matched_prospects) for ix, (courier_ix, route_ix) in enumerate(matched_prospects): courier, route = matching_problem.couriers[ courier_ix], matching_problem.routes[route_ix] instruction = route.stops[ 1:] if courier.condition == 'picking_up' else route notifications[ix] = Notification( courier=courier, instruction=instruction, type=NotificationType.PICK_UP_DROP_OFF) else: notifications = [] for ix, (courier_ix, route_ix) in enumerate(matched_prospects): courier, route = matching_problem.couriers[ courier_ix], matching_problem.routes[route_ix] instruction = route.stops[ 1:] if courier.condition == 'picking_up' else route notification = Notification( courier=courier, instruction=instruction, type=NotificationType.PICK_UP_DROP_OFF) _, time_to_first_stop = OSRMService.estimate_travelling_properties( origin=courier.location, destination=route.stops[0].location, vehicle=courier.vehicle) if isinstance(instruction, list) and courier.condition == 'picking_up': notifications.append(notification) elif courier.condition == 'idle': if route.time_since_ready( env_time ) > settings.DISPATCHER_PROSPECTS_MAX_READY_TIME: notifications.append(notification) elif (time_to_first_stop <= settings.DISPATCHER_PROSPECTS_MAX_STOP_OFFSET and time_to_sec( min(order.ready_time for order in route.orders.values())) <= env_time + settings.DISPATCHER_PROSPECTS_MAX_STOP_OFFSET): notifications.append(notification) elif time_to_first_stop > settings.DISPATCHER_PROSPECTS_MAX_STOP_OFFSET: notifications.append( Notification( courier=courier, instruction=Route(stops=[ Stop(location=route.stops[0].location, type=StopType.PREPOSITION) ]), type=NotificationType.PREPOSITIONING)) return notifications
def test_generate_group_routes(self, osrm): """Test to verify how the heuristic to generate routes work""" # Constants order_1 = Order(order_id=1, pick_up_at=Location(lat=4.678417, lng=-74.054725), drop_off_at=Location(lat=4.717045, lng=-74.036359), ready_time=time(12, 13, 0)) order_2 = Order(order_id=2, pick_up_at=Location(lat=4.678417, lng=-74.054725), drop_off_at=Location(lat=4.723418, lng=-74.037067), ready_time=time(12, 10, 0)) order_3 = Order(order_id=3, pick_up_at=Location(lat=4.678417, lng=-74.054725), drop_off_at=Location(lat=4.723418, lng=-74.037067), ready_time=time(12, 30, 0)) old_order = Order(order_id=9898, pick_up_at=Location(lat=4.678417, lng=-74.054725), drop_off_at=Location(lat=4.727278, lng=-74.039299), ready_time=time(11, 50, 0)) target_size = 2 # Case 1: orders routed without initial routes and courier slack num_idle_couriers = 4 routes = MyopicMatchingPolicy._generate_group_routes( orders=[order_1, order_2], target_size=target_size, courier_routes=[], num_idle_couriers=num_idle_couriers) self.assertTrue(routes) self.assertEqual(len(routes), 2) routed_orders = [o for route in routes for o in route.orders.keys()] for order in [order_1, order_2]: self.assertIn(order.order_id, routed_orders) # Case 2: orders routed without initial routes and no courier slack num_idle_couriers = 0 routes = MyopicMatchingPolicy._generate_group_routes( orders=[order_1, order_2], target_size=target_size, courier_routes=[], num_idle_couriers=num_idle_couriers) self.assertTrue(routes) self.assertEqual(len(routes), 1) routed_orders = [o for route in routes for o in route.orders.keys()] for order in [order_1, order_2]: self.assertIn(order.order_id, routed_orders) # Case 3: orders routed with initial routes and courier slack num_idle_couriers = 1 initial_route = Route(orders={old_order.order_id: old_order}, stops=[ Stop(orders={old_order.order_id: old_order}, location=old_order.pick_up_at, position=0, type=StopType.PICK_UP), Stop(orders={old_order.order_id: old_order}, location=old_order.drop_off_at, position=1, type=StopType.DROP_OFF) ]) routes = MyopicMatchingPolicy._generate_group_routes( orders=[order_1, order_2], target_size=2, courier_routes=[initial_route], num_idle_couriers=num_idle_couriers, max_orders=3, courier_ids=[3]) self.assertTrue(routes) self.assertEqual(len(routes), 1) routed_orders = [o for route in routes for o in route.orders.keys()] for order in [order_1, order_2]: self.assertIn(order.order_id, routed_orders) self.assertIsNone(routes[0].initial_prospect) # Case 4: orders routed without initial routes and insufficient couriers num_idle_couriers = 0 target_size = 5 routes = MyopicMatchingPolicy._generate_group_routes( orders=[order_1, order_2, order_3], target_size=target_size, courier_routes=[], num_idle_couriers=num_idle_couriers) self.assertTrue(routes) self.assertEqual(len(routes), 1) routed_orders = [o for route in routes for o in route.orders.keys()] for order in [order_1, order_2, order_3]: self.assertIn(order.order_id, routed_orders)