Exemple #1
0
    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)
Exemple #2
0
    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
Exemple #3
0
    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)])
Exemple #4
0
    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)))
Exemple #5
0
    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)
Exemple #6
0
    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)
Exemple #7
0
    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])
Exemple #8
0
    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
Exemple #9
0
    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)
Exemple #10
0
    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()
Exemple #11
0
    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]
Exemple #12
0
    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
                    )
                ]
            )
Exemple #13
0
    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
Exemple #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, {})
Exemple #15
0
    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
Exemple #16
0
    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())
Exemple #17
0
    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)
Exemple #18
0
    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)
Exemple #19
0
    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())
Exemple #20
0
    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
Exemple #21
0
    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)
Exemple #22
0
    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())
Exemple #23
0
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)])
Exemple #24
0
    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())
Exemple #25
0
    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)
Exemple #26
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.'
                )
Exemple #27
0
    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())
Exemple #28
0
    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
Exemple #29
0
    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)