diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e03f4b6a..80ebea22 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: rev: 24.8.0 hooks: - id: black - language_version: python3.12 + language_version: python3 - repo: https://github.com/adamchainz/blacken-docs rev: 1.16.0 hooks: diff --git a/src/ridepy/util/dispatchers/__init__.py b/src/ridepy/util/dispatchers/__init__.py index 5b0faf21..a47fb87d 100644 --- a/src/ridepy/util/dispatchers/__init__.py +++ b/src/ridepy/util/dispatchers/__init__.py @@ -1,6 +1,7 @@ from ridepy.util.dispatchers.taxicab import TaxicabDispatcherDriveFirst from ridepy.util.dispatchers.ridepooling import ( BruteForceTotalTravelTimeMinimizingDispatcher, + MinimalPassengerTravelTimeDispatcher, ) """ diff --git a/src/ridepy/util/dispatchers/ridepooling.py b/src/ridepy/util/dispatchers/ridepooling.py index e07d55a4..cf065187 100644 --- a/src/ridepy/util/dispatchers/ridepooling.py +++ b/src/ridepy/util/dispatchers/ridepooling.py @@ -1,5 +1,5 @@ from copy import deepcopy - +from itertools import tee import numpy as np from ridepy.data_structures import ( @@ -209,6 +209,27 @@ def is_timewindow_violated_or_violation_worsened_due_to_insertion( return False +def pairwise(iterable): + # A pairwise iterator. + a, b = tee(iterable) + next(b, None) + return zip(a, b) + + +def is_between(network, a, u, v): + """ + checks if a is on a shortest path between u and v + """ + dist_to = network.t(u, a) + dist_from = network.t(a, v) + dist_direct = network.t(u, v) + is_inbetween = dist_to + dist_from == dist_direct + if is_inbetween: + return True, dist_to, dist_from, dist_direct + else: + return False, dist_to, dist_from, dist_direct + + @dispatcherclass def BruteForceTotalTravelTimeMinimizingDispatcher( request: TransportationRequest, @@ -386,3 +407,188 @@ def BruteForceTotalTravelTimeMinimizingDispatcher( return min_cost, new_stoplist, (EAST_pu, LAST_pu, EAST_do, LAST_do) else: return min_cost, None, (np.nan, np.nan, np.nan, np.nan) + + +@dispatcherclass +def MinimalPassengerTravelTimeDispatcher( + request: TransportationRequest, + stoplist: Stoplist, + space: TransportSpace, + seat_capacity: int, +) -> DispatcherSolution: + """ + The dispatcher optimizes travel time for each passenger. Once a passenger is assigned, their travel time cannot be increased, meaning detours are not permitted. + """ + min_cost = np.inf + bool_insert_end = True + best_pickup_idx = len(stoplist) - 1 + best_dropoff_idx = len(stoplist) - 1 + + for counter in range(0, len(stoplist) - 1): + stop_before_pickup = stoplist[counter] + stop_after_pickup = stoplist[counter + 1] + list_result_in_between_test = is_between( + space, + request.origin, + stop_before_pickup.location, + stop_after_pickup.location, + ) + if ( + list_result_in_between_test[0] == True + and list_result_in_between_test[2] != 0 + ): + if stop_before_pickup.occupancy_after_servicing == seat_capacity: + continue + time_to_pickup = space.t(stop_before_pickup.location, request.origin) + CPAT_pu = cpat_of_inserted_stop(stop_before_pickup, time_to_pickup) + EAST_pu = request.pickup_timewindow_min + if CPAT_pu > request.pickup_timewindow_max: + continue + CPAT_do = max(EAST_pu, CPAT_pu) + space.t( + request.origin, request.destination + ) + if CPAT_do > request.delivery_timewindow_max: + continue + + best_pickup_idx = counter + bool_drop_off_enroute = False + bool_continue_pick_up_loop = False + bool_break_pick_up_loop = False + + list_result_in_between_testFollowingPickUp = is_between( + space, request.destination, request.origin, stop_after_pickup.location + ) + if ( + list_result_in_between_testFollowingPickUp[0] == True + and list_result_in_between_testFollowingPickUp[2] != 0 + ): + best_dropoff_idx = counter + min_cost = CPAT_pu + bool_drop_off_enroute = True + bool_insert_end = False + break + + for counter_drop_off, ( + stop_before_dropoff, + stop_after_dropoff, + ) in enumerate(pairwise(stoplist[best_pickup_idx:])): + if counter_drop_off == best_pickup_idx: + continue + if counter_drop_off < best_pickup_idx: + continue + list_result_in_between_testDropOff = is_between( + space, + request.destination, + stop_before_dropoff.location, + stop_after_dropoff.location, + ) + if ( + list_result_in_between_testDropOff[0] == True + and list_result_in_between_testDropOff[1] != 0 + ): + best_dropoff_idx = counter + counter_drop_off + time_to_dropoff = space.t( + stop_before_dropoff.location, request.destination + ) + CPAT_do = cpat_of_inserted_stop( + stop_before_dropoff, time_to_dropoff, delta_cpat=0 + ) + stoplist_request_in_vehicle = stoplist[ + best_pickup_idx : best_dropoff_idx + 1 + ] + + list_occupancies_after_servicing = [] + for x in stoplist_request_in_vehicle: + list_occupancies_after_servicing.append( + x.occupancy_after_servicing + ) + + if seat_capacity in list_occupancies_after_servicing: + bool_continue_pick_up_loop = True + break + if CPAT_do > request.delivery_timewindow_max: + bool_continue_pick_up_loop = True + break + bool_insert_end = False + min_cost = CPAT_do + bool_drop_off_enroute = True + bool_break_pick_up_loop = True + break + + if bool_break_pick_up_loop: + break + + if bool_continue_pick_up_loop: + best_pickup_idx = len(stoplist) - 1 + best_dropoff_idx = len(stoplist) - 1 + continue + + if not bool_drop_off_enroute: + best_dropoff_idx = len(stoplist) - 1 + stop_before_dropoff = stoplist[-1] + time_to_dropoff = space.t( + stop_before_dropoff.location, request.destination + ) + CPAT_do = cpat_of_inserted_stop( + stop_before_dropoff, time_to_dropoff, delta_cpat=0 + ) + stoplist_request_in_vehicle = stoplist[ + best_pickup_idx : best_dropoff_idx + 1 + ] + + list_occupancies_after_servicing = [] + for x in stoplist_request_in_vehicle: + list_occupancies_after_servicing.append(x.occupancy_after_servicing) + + if seat_capacity in list_occupancies_after_servicing: + best_pickup_idx = len(stoplist) - 1 + best_dropoff_idx = len(stoplist) - 1 + continue + if CPAT_do > request.delivery_timewindow_max: + best_pickup_idx = len(stoplist) - 1 + best_dropoff_idx = len(stoplist) - 1 + continue + bool_insert_end = False + min_cost = CPAT_do + break + else: + continue + + if bool_insert_end: + best_pickup_idx = len(stoplist) - 1 + best_dropoff_idx = len(stoplist) - 1 + time_to_pickup = space.t(stoplist[best_pickup_idx].location, request.origin) + CPAT_pu = cpat_of_inserted_stop(stoplist[best_pickup_idx], time_to_pickup) + EAST_pu = request.pickup_timewindow_min + CPAT_do = max(EAST_pu, CPAT_pu) + space.t(request.origin, request.destination) + if CPAT_pu > request.pickup_timewindow_max: + min_cost = np.inf + elif CPAT_do > request.delivery_timewindow_max: + min_cost = np.inf + else: + min_cost = CPAT_do + + if min_cost < np.inf: + new_stoplist = insert_request_to_stoplist_drive_first( + stoplist=stoplist, + request=request, + pickup_idx=best_pickup_idx, + dropoff_idx=best_dropoff_idx, + space=space, + ) + EAST_pu, LAST_pu = ( + new_stoplist[best_pickup_idx + 1].time_window_min, + new_stoplist[best_pickup_idx + 1].time_window_max, + ) + EAST_do, LAST_do = ( + new_stoplist[best_dropoff_idx + 2].time_window_min, + new_stoplist[best_dropoff_idx + 2].time_window_max, + ) + + listOccupanciesNewStopList = list( + map(lambda x: x.occupancy_after_servicing, new_stoplist) + ) + + return min_cost, new_stoplist, (EAST_pu, LAST_pu, EAST_do, LAST_do) + else: + return min_cost, None, (np.nan, np.nan, np.nan, np.nan) diff --git a/src/ridepy/util/dispatchers_cython/__init__.py b/src/ridepy/util/dispatchers_cython/__init__.py index b96790dc..5b52d089 100644 --- a/src/ridepy/util/dispatchers_cython/__init__.py +++ b/src/ridepy/util/dispatchers_cython/__init__.py @@ -1,4 +1,5 @@ from .dispatchers import ( BruteForceTotalTravelTimeMinimizingDispatcher, + MinimalPassengerTravelTimeDispatcher, SimpleEllipseDispatcher, ) diff --git a/src/ridepy/util/dispatchers_cython/cdispatchers.h b/src/ridepy/util/dispatchers_cython/cdispatchers.h index f70f644e..33e0e304 100644 --- a/src/ridepy/util/dispatchers_cython/cdispatchers.h +++ b/src/ridepy/util/dispatchers_cython/cdispatchers.h @@ -369,6 +369,247 @@ template class AbstractDispatcher { virtual ~AbstractDispatcher(){}; }; +template +InsertionResult minimal_passenger_travel_time_dispatcher( + std::shared_ptr> request, + vector> &stoplist, TransportSpace &space, int seat_capacity, + bool debug = false) { + + double min_cost = INFINITY; + bool boolInsertEnd = true; + int best_pickup_idx = stoplist.size() - 1; + int best_dropoff_idx = stoplist.size() - 1; + + // printf("------------ New request ------------\n"); + // printf("Request origin: %i\n", request->origin); + // printf("Request destination: %i\n", request->destination); + // printf("Stoplist: "); + // for (auto i: stoplist) { + // std::cout << i.location << ' '; + // } + // printf("\n"); + + for (int counter = 0; counter < stoplist.size() - 1; counter++) { + // printf("Counter: %i\n", counter); + Stop stop_before_pickup = stoplist[counter]; + Stop stop_after_pickup = stoplist[counter + 1]; + auto listResultInBetweenTest = is_between( + space, request->origin, stop_before_pickup, stop_after_pickup); + if (std::get<0>(listResultInBetweenTest) == true && + std::get<2>(listResultInBetweenTest) != 0) { + // printf("stop_before_pickup.occupancy_after_servicing %i\n", + // stop_before_pickup.occupancy_after_servicing); printf("seat_capacity + // %i\n", seat_capacity); + if (stop_before_pickup.occupancy_after_servicing == seat_capacity) { + continue; + } + double time_to_pickup = + space.t(stop_before_pickup.location, request->origin); + double CPAT_pu = + cpat_of_inserted_stop(stop_before_pickup, time_to_pickup); + double EAST_pu = request->pickup_timewindow_min; + if (CPAT_pu > request->pickup_timewindow_max) { + continue; + } + // dropoff immediately + double CPAT_do = max(EAST_pu, CPAT_pu) + + space.t(request->origin, request->destination); + + if (CPAT_do > request->delivery_timewindow_max) + continue; + best_pickup_idx = counter; + // printf("Best pickup idx: %i\n", best_pickup_idx); + bool boolDropOffEnroute = false; + bool boolContinuePickUpLoop = false; + bool boolBreakPickUpLoop = false; + + auto listResultInBetweenTestFollowingPickUp = is_between_2( + space, request->destination, request->origin, stop_after_pickup); + if (std::get<0>(listResultInBetweenTestFollowingPickUp) == true && + std::get<2>(listResultInBetweenTestFollowingPickUp) != 0) { + best_dropoff_idx = counter; + min_cost = CPAT_pu; + boolDropOffEnroute = true; + boolInsertEnd = false; + break; + } + + for (int counter_drop_off = best_pickup_idx; + counter_drop_off < stoplist.size() - 1; counter_drop_off++) { + // printf("Counter drop off: %i\n", counter_drop_off); + Stop stop_before_dropoff = stoplist[counter_drop_off]; + Stop stop_after_dropoff = stoplist[counter_drop_off + 1]; + if (counter_drop_off == counter) { + continue; + } + auto listResultInBetweenTestDropOff = + is_between(space, request->destination, stop_before_dropoff, + stop_after_dropoff); + // Changed from 2 to 1 + + if (std::get<0>(listResultInBetweenTestDropOff) == true && + std::get<2>(listResultInBetweenTestDropOff) != 0) { + best_dropoff_idx = counter_drop_off; + double time_to_dropoff = + space.t(stop_before_dropoff.location, request->destination); + double CPAT_do = + cpat_of_inserted_stop(stop_before_dropoff, time_to_dropoff); + std::vector> stoplist_request_in_vehicle = { + stoplist.begin() + best_pickup_idx, + stoplist.begin() + best_dropoff_idx + 1}; + + // printf("Stoplist Request In Vehicle: "); + // for (auto i: stoplist_request_in_vehicle) { + // std::cout << i.location << ' '; + // } + // printf("\n"); + + std::vector occupancies_ausschnitt; + std::transform(stoplist_request_in_vehicle.begin(), + stoplist_request_in_vehicle.end(), + std::back_inserter(occupancies_ausschnitt), + [](auto x) { return x.occupancy_after_servicing; }); + + // printf("Occupancies: "); + // for (auto i: occupancies_ausschnitt) { + // std::cout << i << ' '; + // } + // printf("\n"); + + if (std::count(occupancies_ausschnitt.begin(), + occupancies_ausschnitt.end(), seat_capacity)) { + // printf("True \n"); + boolContinuePickUpLoop = true; + break; + } + + if (CPAT_do > request->delivery_timewindow_max) { + boolContinuePickUpLoop = true; + break; + } + boolInsertEnd = false; + min_cost = CPAT_do; + boolDropOffEnroute = true; + boolBreakPickUpLoop = true; + break; + } + } + + if (boolBreakPickUpLoop == true) { + break; + } + + if (boolContinuePickUpLoop == true) { + best_pickup_idx = stoplist.size() - 1; + best_dropoff_idx = stoplist.size() - 1; + continue; + } + + if (boolDropOffEnroute == false) { + // printf("boolDropOffEnroute"); + // printf("\n"); + best_dropoff_idx = stoplist.size() - 1; + Stop stop_before_dropoff = stoplist[best_dropoff_idx]; + double time_to_dropoff = + space.t(stop_before_dropoff.location, request->destination); + double CPAT_do = + cpat_of_inserted_stop(stop_before_dropoff, time_to_dropoff); + std::vector> stoplist_request_in_vehicle = { + stoplist.begin() + best_pickup_idx, + stoplist.begin() + best_dropoff_idx + 1}; + + // printf("Stoplist Request In Vehicle: "); + // for (auto i: stoplist_request_in_vehicle) { + // std::cout << i.location << ' '; + // } + // printf("\n"); + + std::vector occupancies_ausschnitt; + std::transform(stoplist_request_in_vehicle.begin(), + stoplist_request_in_vehicle.end(), + std::back_inserter(occupancies_ausschnitt), + [](auto x) { return x.occupancy_after_servicing; }); + + // printf("Occupancies: "); + // for (auto i: occupancies_ausschnitt) { + // std::cout << i << ' '; + // } + // printf("\n"); + + if (std::count(occupancies_ausschnitt.begin(), + occupancies_ausschnitt.end(), seat_capacity)) { + // printf("True \n"); + best_pickup_idx = stoplist.size() - 1; + best_dropoff_idx = stoplist.size() - 1; + continue; + } + + if (CPAT_do > request->delivery_timewindow_max) { + best_pickup_idx = stoplist.size() - 1; + best_dropoff_idx = stoplist.size() - 1; + continue; + } + boolInsertEnd = false; + min_cost = CPAT_do; + break; + } + } else { + continue; + } + } + + if (boolInsertEnd == true) { + // both pickup and dropoff have to be appended + // printf("boolInsertEnd"); + // printf("\n"); + best_pickup_idx = stoplist.size() - 1; + best_dropoff_idx = stoplist.size() - 1; + double time_to_pickup = + space.t(stoplist[best_pickup_idx].location, request->origin); + double CPAT_pu = + cpat_of_inserted_stop(stoplist[best_pickup_idx], time_to_pickup); + double EAST_pu = request->pickup_timewindow_min; + double CPAT_do = + max(EAST_pu, CPAT_pu) + space.t(request->origin, request->destination); + if (CPAT_pu > request->pickup_timewindow_max) { + min_cost = INFINITY; + } else { + if (CPAT_do > request->delivery_timewindow_max) { + min_cost = INFINITY; + } else { + min_cost = CPAT_do; + } + } + } + + // printf("best_pickup_idx %i\n", best_pickup_idx); + // printf("best_dropoff_idx %i\n", best_dropoff_idx); + // printf("min_cost %f\n", min_cost); + + if (min_cost < INFINITY) { + auto new_stoplist = insert_request_to_stoplist_drive_first( + stoplist, request, best_pickup_idx, best_dropoff_idx, space); + + // printf("Reached insert_request_to_stoplist_drive_first\n"); + + // printf("New stoplist: "); + // for (auto i: new_stoplist) { + // std::cout << i.location << ' '; + // } + // printf("\n"); + + auto EAST_pu = new_stoplist[best_pickup_idx + 1].time_window_min; + auto LAST_pu = new_stoplist[best_pickup_idx + 1].time_window_max; + + auto EAST_do = new_stoplist[best_dropoff_idx + 2].time_window_min; + auto LAST_do = new_stoplist[best_dropoff_idx + 2].time_window_max; + return InsertionResult{new_stoplist, min_cost, EAST_pu, + LAST_pu, EAST_do, LAST_do}; + } else { + return InsertionResult{{}, min_cost, NAN, NAN, NAN, NAN}; + } +}; + // Pattern motivated by: https://stackoverflow.com/a/2592270 template @@ -384,6 +625,18 @@ class BruteForceTotalTravelTimeMinimizingDispatcher } }; +template +class MinimalPassengerTravelTimeDispatcher : public AbstractDispatcher { +public: + InsertionResult + operator()(std::shared_ptr> request, + vector> &stoplist, TransportSpace &space, + int seat_capacity, bool debug = false) { + return minimal_passenger_travel_time_dispatcher(request, stoplist, space, + seat_capacity, debug); + } +}; + template class SimpleEllipseDispatcher : public AbstractDispatcher { public: diff --git a/src/ridepy/util/dispatchers_cython/cdispatchers.pxd b/src/ridepy/util/dispatchers_cython/cdispatchers.pxd index 828b5d11..8502ea53 100644 --- a/src/ridepy/util/dispatchers_cython/cdispatchers.pxd +++ b/src/ridepy/util/dispatchers_cython/cdispatchers.pxd @@ -12,6 +12,11 @@ cdef extern from "cdispatchers.h" namespace 'ridepy': shared_ptr[TransportationRequest[Loc]] request, vector[Stop[Loc]] &stoplist, const TransportSpace &space, int seat_capacity, bint debug) + + InsertionResult[Loc] minimal_passenger_travel_time_dispatcher[Loc]( + shared_ptr[TransportationRequest[Loc]] request, + vector[Stop[Loc]] &stoplist, + const TransportSpace &space, int seat_capacity, bint debug) InsertionResult[Loc] simple_ellipse_dispatcher[Loc]( shared_ptr[TransportationRequest[Loc]] request, @@ -28,5 +33,8 @@ cdef extern from "cdispatchers.h" namespace 'ridepy': cdef cppclass BruteForceTotalTravelTimeMinimizingDispatcher[Loc](AbstractDispatcher[Loc]): BruteForceTotalTravelTimeMinimizingDispatcher() + cdef cppclass MinimalPassengerTravelTimeDispatcher[Loc](AbstractDispatcher[Loc]): + MinimalPassengerTravelTimeDispatcher() + cdef cppclass SimpleEllipseDispatcher[Loc](AbstractDispatcher[Loc]): SimpleEllipseDispatcher(double) diff --git a/src/ridepy/util/dispatchers_cython/cdispatchers_utils.h b/src/ridepy/util/dispatchers_cython/cdispatchers_utils.h index b7cf0b9f..03540357 100644 --- a/src/ridepy/util/dispatchers_cython/cdispatchers_utils.h +++ b/src/ridepy/util/dispatchers_cython/cdispatchers_utils.h @@ -6,6 +6,7 @@ #define RIDEPY_CDISPATCHERS_UTILS_H #include "../../data_structures_cython/cdata_structures.h" +#include "../spaces_cython/cspaces.h" namespace ridepy { @@ -219,6 +220,36 @@ bool is_timewindow_violated_dueto_insertion( return false; } +template +std::tuple +is_between(TransportSpace &space, const Loc a, Stop &stop_before, + Stop &stop_after) { + double dist_to = space.t(stop_before.location, a); + double dist_from = space.t(a, stop_after.location); + double dist_direct = space.t(stop_before.location, stop_after.location); + bool is_inbetween = dist_to + dist_from == dist_direct; + if (is_inbetween) { + return std::make_tuple(true, dist_to, dist_from, dist_direct); + } else { + return std::make_tuple(false, dist_to, dist_from, dist_direct); + } +} + +template +std::tuple +is_between_2(TransportSpace &space, const Loc a, const Loc u, + Stop &stop_after) { + double dist_to = space.t(u, a); + double dist_from = space.t(a, stop_after.location); + double dist_direct = space.t(u, stop_after.location); + bool is_inbetween = dist_to + dist_from == dist_direct; + if (is_inbetween) { + return std::make_tuple(true, dist_to, dist_from, dist_direct); + } else { + return std::make_tuple(false, dist_to, dist_from, dist_direct); + } +} + } // namespace ridepy -#endif // RIDEPY_CDISPATCHERS_UTILS_H +#endif // RIDEPY_CDISPATCHERS_UTILS_H \ No newline at end of file diff --git a/src/ridepy/util/dispatchers_cython/dispatchers.pyx b/src/ridepy/util/dispatchers_cython/dispatchers.pyx index 01487922..ccef9b5b 100644 --- a/src/ridepy/util/dispatchers_cython/dispatchers.pyx +++ b/src/ridepy/util/dispatchers_cython/dispatchers.pyx @@ -5,6 +5,7 @@ from ridepy.data_structures_cython.data_structures cimport LocType, R2loc, uilo from .cdispatchers cimport ( AbstractDispatcher as CAbstractDispatcher, BruteForceTotalTravelTimeMinimizingDispatcher as CBruteForceTotalTravelTimeMinimizingDispatcher, +MinimalPassengerTravelTimeDispatcher as CMinimalPassengerTravelTimeDispatcher, SimpleEllipseDispatcher as CSimpleEllipseDispatcher, ) @@ -38,6 +39,18 @@ cdef class BruteForceTotalTravelTimeMinimizingDispatcher(Dispatcher): else: raise ValueError("This line should never have been reached") +cdef class MinimalPassengerTravelTimeDispatcher(Dispatcher): + def __cinit__(self, loc_type): + if loc_type == LocType.R2LOC: + self.u_dispatcher.dispatcher_r2loc_ptr = ( + new CMinimalPassengerTravelTimeDispatcher[R2loc]() + ) + elif loc_type == LocType.INT: + self.u_dispatcher.dispatcher_int_ptr = ( + new CMinimalPassengerTravelTimeDispatcher[uiloc]() + ) + else: + raise ValueError("This line should never have been reached") cdef class SimpleEllipseDispatcher(Dispatcher): def __cinit__(self, loc_type, max_relative_detour=0): diff --git a/src/ridepy/util/testing_utils.py b/src/ridepy/util/testing_utils.py index 636f6edc..aca1a58c 100644 --- a/src/ridepy/util/testing_utils.py +++ b/src/ridepy/util/testing_utils.py @@ -4,6 +4,7 @@ from ridepy.util.spaces_cython import TransportSpace as CyTransportSpace from ridepy.util.testing_utils_cython import ( BruteForceTotalTravelTimeMinimizingDispatcher as CyBruteForceTotalTravelTimeMinimizingDispatcher, + MinimalPassengerTravelTimeDispatcher as CyMinimalPassengerTravelTimeDispatcher, ) from ridepy.util.spaces_cython import spaces as cyspaces from typing import Literal, Iterable, Union, Callable, Sequence @@ -13,6 +14,7 @@ from ridepy.util import spaces as pyspaces from ridepy.util.dispatchers.ridepooling import ( BruteForceTotalTravelTimeMinimizingDispatcher, + MinimalPassengerTravelTimeDispatcher, ) @@ -133,3 +135,56 @@ def setup_insertion_data_structures( ) return space, request, stoplist, dispatcher(loc_type=space.loc_type) + + +def setup_insertion_data_structures_minimal_passenger_travel_time_dispatcher( + *, + stoplist_properties: Iterable[Sequence[Union[Location, float]]], + request_properties, + space_type: str, + kind: str, +) -> tuple[ + Union[TransportSpace, CyTransportSpace], + Union[pyds.TransportationRequest, cyds.TransportationRequest], + Union[Stoplist, cyds.Stoplist], + Dispatcher, +]: + """ + Function is specified for testing the MinimalPassengerTravelTimeDispatcher + #FIXME Merge functions or rename above function + + Parameters + ---------- + stoplist_properties + request_properties + space_type + kind + 'cython' or 'python' + + Returns + ------- + space, request, stoplist, dispatcher + """ + + if kind == "python": + spaces = pyspaces + ds = pyds + dispatcher = MinimalPassengerTravelTimeDispatcher + elif kind == "cython": + spaces = cyspaces + ds = cyds + dispatcher = CyMinimalPassengerTravelTimeDispatcher + else: + raise ValueError(f"Supplied invalid {kind=}, must be 'python' or 'cython'") + + space = getattr(spaces, space_type)() + + # set up the request + request = ds.TransportationRequest(**request_properties) + + # set up the stoplist + stoplist = stoplist_from_properties( + stoplist_properties=stoplist_properties, space=space, kind=kind + ) + + return space, request, stoplist, dispatcher(loc_type=space.loc_type) diff --git a/src/ridepy/util/testing_utils_cython/__init__.py b/src/ridepy/util/testing_utils_cython/__init__.py index b96790dc..5b52d089 100644 --- a/src/ridepy/util/testing_utils_cython/__init__.py +++ b/src/ridepy/util/testing_utils_cython/__init__.py @@ -1,4 +1,5 @@ from .dispatchers import ( BruteForceTotalTravelTimeMinimizingDispatcher, + MinimalPassengerTravelTimeDispatcher, SimpleEllipseDispatcher, ) diff --git a/src/ridepy/util/testing_utils_cython/dispatchers.pyx b/src/ridepy/util/testing_utils_cython/dispatchers.pyx index b6c1a997..d2b80ee2 100644 --- a/src/ridepy/util/testing_utils_cython/dispatchers.pyx +++ b/src/ridepy/util/testing_utils_cython/dispatchers.pyx @@ -20,6 +20,7 @@ from ridepy.data_structures_cython.cdata_structures cimport ( from ridepy.util.dispatchers_cython.cdispatchers cimport ( brute_force_total_traveltime_minimizing_dispatcher as c_brute_force_total_traveltime_minimizing_dispatcher, + minimal_passenger_travel_time_dispatcher as c_minimal_passenger_travel_time_dispatcher, simple_ellipse_dispatcher as c_simple_ellipse_dispatcher ) @@ -85,6 +86,44 @@ cdef class BruteForceTotalTravelTimeMinimizingDispatcher: else: raise ValueError("This line should never have been reached") +cdef class MinimalPassengerTravelTimeDispatcher: + cdef LocType loc_type + + def __init__(self, loc_type): + self.loc_type = loc_type + + def __call__( + self, + TransportationRequest cy_request, + Stoplist stoplist, + TransportSpace space, + int seat_capacity, + bint debug=False + ): + cdef InsertionResult[R2loc] insertion_result_r2loc + cdef InsertionResult[uiloc] insertion_result_int + + if self.loc_type == LocType.R2LOC: + insertion_result_r2loc = c_minimal_passenger_travel_time_dispatcher[R2loc]( + dynamic_pointer_cast[CTransportationRequest[R2loc], CRequest[R2loc]](cy_request._ureq._req_r2loc), + stoplist.ustoplist._stoplist_r2loc, + dereference(space.u_space.space_r2loc_ptr), seat_capacity, debug + ) + return insertion_result_r2loc.min_cost, Stoplist.from_c_r2loc(insertion_result_r2loc.new_stoplist), \ + (insertion_result_r2loc.EAST_pu, insertion_result_r2loc.LAST_pu, + insertion_result_r2loc.EAST_do, insertion_result_r2loc.LAST_do) + elif self.loc_type == LocType.INT: + insertion_result_int = c_minimal_passenger_travel_time_dispatcher[uiloc]( + dynamic_pointer_cast[CTransportationRequest[uiloc], CRequest[uiloc]](cy_request._ureq._req_int), + stoplist.ustoplist._stoplist_int, + dereference(space.u_space.space_int_ptr), seat_capacity, debug + ) + return insertion_result_int.min_cost, Stoplist.from_c_int(insertion_result_int.new_stoplist), \ + (insertion_result_int.EAST_pu, insertion_result_int.LAST_pu, + insertion_result_int.EAST_do, insertion_result_int.LAST_do) + else: + raise ValueError("This line should never have been reached") + cdef class SimpleEllipseDispatcher: cdef LocType loc_type diff --git a/test/test_minimal_passenger_travel_time_dispatcher.py b/test/test_minimal_passenger_travel_time_dispatcher.py new file mode 100644 index 00000000..b1778a72 --- /dev/null +++ b/test/test_minimal_passenger_travel_time_dispatcher.py @@ -0,0 +1,428 @@ +import numpy as np +import pytest + +import itertools as it + +from numpy import inf, isclose + +from ridepy.events import ( + RequestRejectionEvent, + PickupEvent, + DeliveryEvent, +) +from ridepy.util.dispatchers.ridepooling import ( + MinimalPassengerTravelTimeDispatcher, +) +from ridepy.extras.spaces import make_nx_grid +from ridepy.util.request_generators import RandomRequestGenerator +from ridepy.util.spaces import Euclidean2D, Graph +from ridepy.fleet_state import SlowSimpleFleetState +from ridepy.util.testing_utils import ( + setup_insertion_data_structures_minimal_passenger_travel_time_dispatcher, +) +from ridepy.vehicle_state import VehicleState + + +@pytest.mark.parametrize("kind", ["python", "cython"]) +def test_append_to_empty_stoplist(kind): + request_properties = dict( + request_id=42, + creation_timestamp=1, + origin=(0, 1), + destination=(0, 2), + pickup_timewindow_min=0, + pickup_timewindow_max=inf, + delivery_timewindow_min=0, + delivery_timewindow_max=inf, + ) + + # location, cpat, tw_min, tw_max, occupancy + stoplist_properties = [[(0, 0), 0, 0, inf]] + ( + space, + request, + stoplist, + minimal_passenger_travel_time_dispatcher, + ) = setup_insertion_data_structures_minimal_passenger_travel_time_dispatcher( + stoplist_properties=stoplist_properties, + request_properties=request_properties, + space_type="Euclidean2D", + kind=kind, + ) + min_cost, new_stoplist, *_ = minimal_passenger_travel_time_dispatcher( + request, stoplist, space, seat_capacity=10 + ) + assert new_stoplist[-2].location == request.origin + assert new_stoplist[-1].location == request.destination + + +@pytest.mark.parametrize("kind", ["python", "cython"]) +def test_no_solution_found(kind): + """Test that if no solution exists, none is returned""" + # FIXME: Unclear, how to use the Manhatten graph with shortest paths. + # fmt: off + # location, cpat, tw_min, tw_max, occupancy + stoplist_properties = [ + [(0, 1), 1, 1, 1], + [(0, 3), 3, 3, 3] + ] + # fmt: on + eps = 1e-4 + request_properties = dict( + request_id=42, + creation_timestamp=1, + origin=(eps, 1), + destination=(eps, 2), + pickup_timewindow_min=0, + pickup_timewindow_max=eps / 2, + delivery_timewindow_min=0, + delivery_timewindow_max=inf, + ) + ( + space, + request, + stoplist, + minimal_passenger_travel_time_dispatcher, + ) = setup_insertion_data_structures_minimal_passenger_travel_time_dispatcher( + stoplist_properties=stoplist_properties, + request_properties=request_properties, + space_type="Manhattan2D", + kind=kind, + ) + + ( + min_cost, + new_stoplist, + timewindows, + ) = minimal_passenger_travel_time_dispatcher( + request, stoplist, space, seat_capacity=10 + ) + assert np.isinf(min_cost) + assert not new_stoplist # an empty `Stoplist` for cython, None for python + assert np.isnan(timewindows).all() + + # But the same shouldn't occur if the tw_max were higher: + # FIXME: It is unclear, if this is true for the minimal passenger travel time dispatcher + stoplist_properties = [ + [(0, 1), 1, 1, 0], + [(0, 3), 3, 3, 3 + 3 * eps], # tw_max just enough + ] + request_properties = dict( + request_id=42, + creation_timestamp=1, + origin=(eps, 1), + destination=(eps, 2), + pickup_timewindow_min=0, + pickup_timewindow_max=1 + 2 * eps, # just enough + delivery_timewindow_min=0, + delivery_timewindow_max=inf, + ) + ( + space, + request, + stoplist, + minimal_passenger_travel_time_dispatcher, + ) = setup_insertion_data_structures_minimal_passenger_travel_time_dispatcher( + stoplist_properties=stoplist_properties, + request_properties=request_properties, + space_type="Manhattan2D", + kind=kind, + ) + min_cost, new_stoplist, *_ = minimal_passenger_travel_time_dispatcher( + request, stoplist, space, seat_capacity=10 + ) + # assert not np.isinf(min_cost) + + +@pytest.mark.parametrize("kind", ["python", "cython"]) +def test_append_due_to_pickup_not_on_shortest_path(kind): + # fmt: off + # location, cpat, tw_min, tw_max, occupancy + stoplist_properties = [ + [(0, 1), 1, 0, inf], + [(0, 3), 3, 3, 3] + ] + # fmt: on + eps = 1e-4 + request_properties = dict( + request_id=42, + creation_timestamp=1, + origin=(eps, 2), + destination=(0, 4), + pickup_timewindow_min=0, + pickup_timewindow_max=inf, + delivery_timewindow_min=0, + delivery_timewindow_max=inf, + ) + ( + space, + request, + stoplist, + minimal_passenger_travel_time_dispatcher, + ) = setup_insertion_data_structures_minimal_passenger_travel_time_dispatcher( + stoplist_properties=stoplist_properties, + request_properties=request_properties, + space_type="Euclidean2D", + kind=kind, + ) + + min_cost, new_stoplist, *_ = minimal_passenger_travel_time_dispatcher( + request, stoplist, space, seat_capacity=10 + ) + assert np.allclose(new_stoplist[-2].location, request.origin) + assert np.allclose(new_stoplist[-1].location, request.destination) + + assert [s.occupancy_after_servicing for s in new_stoplist] == [0, 0, 1, 0] + + +@pytest.mark.parametrize("kind", ["python", "cython"]) +def test_inserted_at_the_middle(kind): + # fmt: off + # location, cpat, tw_min, tw_max, occupancy + stoplist_properties = [ + [(0, 1), 1, 0, inf], + [(0, 3), 3, 0, 6], + ] + # fmt: on + eps = 1e-4 + request_properties = dict( + request_id=42, + creation_timestamp=1, + origin=(0, 1.5), + destination=(0, 2.5), + pickup_timewindow_min=0, + pickup_timewindow_max=inf, + delivery_timewindow_min=0, + delivery_timewindow_max=inf, + ) + ( + space, + request, + stoplist, + minimal_passenger_travel_time_dispatcher, + ) = setup_insertion_data_structures_minimal_passenger_travel_time_dispatcher( + stoplist_properties=stoplist_properties, + request_properties=request_properties, + space_type="Euclidean2D", + kind=kind, + ) + + min_cost, new_stoplist, *_ = minimal_passenger_travel_time_dispatcher( + request, stoplist, space, seat_capacity=10 + ) + assert new_stoplist[1].location == request.origin + assert new_stoplist[2].location == request.destination + assert [s.occupancy_after_servicing for s in new_stoplist] == [0, 1, 0, 0] + + +@pytest.mark.parametrize("kind", ["python", "cython"]) +def test_inserted_separately(kind): + # fmt: off + # location, cpat, tw_min, tw_max, occupancy + stoplist_properties = [ + [(0, 1), 1, 0, inf], + [(0, 3), 3, 0, inf], + [(0, 5), 5, 0, inf], + [(0, 7), 7, 0, inf], + ] + # fmt: on + request_properties = dict( + request_id=42, + creation_timestamp=1, + origin=(0, 2), + destination=(0, 4), + pickup_timewindow_min=0, + pickup_timewindow_max=inf, + delivery_timewindow_min=0, + delivery_timewindow_max=inf, + ) + ( + space, + request, + stoplist, + minimal_passenger_travel_time_dispatcher, + ) = setup_insertion_data_structures_minimal_passenger_travel_time_dispatcher( + stoplist_properties=stoplist_properties, + request_properties=request_properties, + space_type="Euclidean2D", + kind=kind, + ) + min_cost, new_stoplist, *_ = minimal_passenger_travel_time_dispatcher( + request, stoplist, space, seat_capacity=10 + ) + assert new_stoplist[1].location == request.origin + assert new_stoplist[3].location == request.destination + assert [s.occupancy_after_servicing for s in new_stoplist] == [0, 1, 1, 0, 0, 0] + + +@pytest.mark.parametrize("kind", ["python", "cython"]) +def test_not_inserted_separately_dueto_capacity_constraint(kind): + """ + Forces the pickup and dropoff to be inserted together solely because + of seat_capacity=1 + """ + # fmt: off + # location, cpat, tw_min, tw_max, occupancy + stoplist_properties = [ + [(0, 1), 1, 0, inf], + [(0, 3), 3, 0, inf], + [(0, 5), 5, 0, inf], + [(0, 7), 7, 0, inf], + ] + # fmt: on + request_properties = dict( + request_id=42, + creation_timestamp=1, + origin=(0, 1.5), + destination=(0, 2.5), + pickup_timewindow_min=0, + pickup_timewindow_max=inf, + delivery_timewindow_min=0, + delivery_timewindow_max=inf, + ) + # the best insertion would be [s0, +, -, s1, s2, s3] + ( + space, + request, + stoplist, + minimal_passenger_travel_time_dispatcher, + ) = setup_insertion_data_structures_minimal_passenger_travel_time_dispatcher( + stoplist_properties=stoplist_properties, + request_properties=request_properties, + space_type="Euclidean2D", + kind=kind, + ) + + for s, cap in zip(stoplist, [0, 1, 0, 1]): + s.occupancy_after_servicing = cap + + min_cost, new_stoplist, *_ = minimal_passenger_travel_time_dispatcher( + request, stoplist, space, seat_capacity=1 + ) + assert new_stoplist[1].location == request.origin + assert new_stoplist[2].location == request.destination + assert [s.occupancy_after_servicing for s in new_stoplist] == [0, 1, 0, 1, 0, 1] + + ( + space, + request, + stoplist, + minimal_passenger_travel_time_dispatcher, + ) = setup_insertion_data_structures_minimal_passenger_travel_time_dispatcher( + stoplist_properties=stoplist_properties, + request_properties=request_properties, + space_type="Euclidean2D", + kind=kind, + ) + + # the best insertion would be [s0, s1, s2, s3, +, -,] + for s, cap in zip(stoplist, [1, 1, 1, 0]): + s.occupancy_after_servicing = cap + + min_cost, new_stoplist, *_ = minimal_passenger_travel_time_dispatcher( + request, stoplist, space, seat_capacity=1 + ) + assert new_stoplist[4].location == request.origin + assert new_stoplist[5].location == request.destination + assert [s.occupancy_after_servicing for s in new_stoplist] == [1, 1, 1, 0, 1, 0] + + +@pytest.mark.parametrize("kind", ["python", "cython"]) +def test_stoplist_not_modified_inplace(kind): + # fmt: off + # location, cpat, tw_min, tw_max, occupancy + stoplist_properties = [ + [(0, 1), 1, 0, inf], + [(0, 3), 3, 0, 6], + ] + # fmt: on + eps = 0 + request_properties = dict( + request_id=42, + creation_timestamp=1, + origin=(eps, 1.5), + destination=(eps, 2.5), + pickup_timewindow_min=0, + pickup_timewindow_max=inf, + delivery_timewindow_min=0, + delivery_timewindow_max=inf, + ) + ( + space, + request, + stoplist, + minimal_passenger_travel_time_dispatcher, + ) = setup_insertion_data_structures_minimal_passenger_travel_time_dispatcher( + stoplist_properties=stoplist_properties, + request_properties=request_properties, + space_type="Euclidean2D", + kind=kind, + ) + + min_cost, new_stoplist, *_ = minimal_passenger_travel_time_dispatcher( + request, stoplist, space, seat_capacity=10 + ) + assert new_stoplist[1].location == request.origin + assert new_stoplist[2].location == request.destination + assert new_stoplist[3].estimated_arrival_time == 3 + 2 * eps + assert stoplist[1].estimated_arrival_time == 3 + + +def test_sanity_in_graph(): + """ + Insert a request, note delivery time. + Handle more requests so that there's no pooling. + Assert that the delivery time is not changed. + + Or more simply, assert that the vehicle moves at either the space's velocity or 0. + """ + + for velocity in [0.9, 1, 1.1]: + space = Graph.from_nx(make_nx_grid(), velocity=velocity) + + rg = RandomRequestGenerator( + rate=10, + space=space, + max_delivery_delay_abs=0, + ) + + transportation_requests = list(it.islice(rg, 10000)) + + fs = SlowSimpleFleetState( + initial_locations={k: 0 for k in range(50)}, + seat_capacities=10, + space=space, + dispatcher=MinimalPassengerTravelTimeDispatcher(), + vehicle_state_class=VehicleState, + ) + + events = list(fs.simulate(transportation_requests)) + + rejections = set( + ev["request_id"] + for ev in events + if ev["event_type"] == "RequestRejectionEvent" + ) + pickup_times = { + ev["request_id"]: ev["timestamp"] + for ev in events + if ev["event_type"] == "PickupEvent" + } + delivery_times = { + ev["request_id"]: ev["timestamp"] + for ev in events + if ev["event_type"] == "DeliveryEvent" + } + + assert len(transportation_requests) > len(rejections) + for req in transportation_requests: + if (rid := req.request_id) not in rejections: + assert isclose(req.delivery_timewindow_max, delivery_times[rid]) + assert isclose( + delivery_times[rid] - pickup_times[rid], + space.t(req.origin, req.destination), + ) + + +if __name__ == "__main__": + pytest.main(args=[__file__]) diff --git a/test/test_minimal_passenger_travel_time_dispatcher_cython.py b/test/test_minimal_passenger_travel_time_dispatcher_cython.py new file mode 100644 index 00000000..1ff99336 --- /dev/null +++ b/test/test_minimal_passenger_travel_time_dispatcher_cython.py @@ -0,0 +1,261 @@ +import random +import pytest + +import numpy as np +import itertools as it + +from numpy import inf, isclose +from time import time +from pandas.core.common import flatten + +from ridepy.data_structures_cython import Stoplist as CyStoplist + +from ridepy import data_structures_cython as cyds +from ridepy import data_structures as pyds + +from ridepy.events import ( + RequestRejectionEvent, + PickupEvent, + DeliveryEvent, +) + +from ridepy.data_structures_cython.data_structures import LocType +from ridepy.util import spaces as pyspaces +from ridepy.util.spaces_cython import spaces as cyspaces +from ridepy.util.request_generators import RandomRequestGenerator + +from ridepy.util.dispatchers.ridepooling import ( + MinimalPassengerTravelTimeDispatcher, +) +from ridepy.util.dispatchers_cython import ( + MinimalPassengerTravelTimeDispatcher as CMinimalPassengerTravelTimeDispatcher, +) +from ridepy.util.testing_utils_cython import ( + MinimalPassengerTravelTimeDispatcher as CyMinimalPassengerTravelTimeDispatcher, +) +from ridepy.util.testing_utils import stoplist_from_properties +from ridepy.vehicle_state import VehicleState as py_VehicleState +from ridepy.vehicle_state_cython import VehicleState as cy_VehicleState + +from ridepy.fleet_state import SlowSimpleFleetState +from ridepy.extras.spaces import make_nx_grid + + +def test_equivalence_cython_and_python_minimal_passenger_travel_time_dispatcher( + seed=42, +): + """ + Tests that the pure pythonic and cythonic minimal passenger travel time dispatcher produces identical results. + """ + len_stoplist = 100 + seat_capacity = 4 + rnd = np.random.RandomState(seed) + stop_locations = rnd.uniform(low=0, high=100, size=(len_stoplist, 2)) + arrival_times = np.cumsum( + [np.linalg.norm(x - y) for x, y in zip(stop_locations[:-1], stop_locations[1:])] + ) + arrival_times = np.insert(arrival_times, 0, 0) + # location, CPAT, tw_min, tw_max, + stoplist_properties = [ + [stop_loc, CPAT, 0, inf] + for stop_loc, CPAT in zip(stop_locations, arrival_times) + ] + origin = list(rnd.uniform(low=0, high=100, size=2)) + destination = list(rnd.uniform(low=0, high=100, size=2)) + + # first call the pure pythonic dispatcher + request = pyds.TransportationRequest( + request_id=100, + creation_timestamp=1, + origin=origin, + destination=destination, + pickup_timewindow_min=0, + pickup_timewindow_max=inf, + delivery_timewindow_min=0, + delivery_timewindow_max=inf, + ) + space = pyspaces.Euclidean2D() + stoplist = stoplist_from_properties( + stoplist_properties=stoplist_properties, kind="python", space=space + ) + + tick = time() + # min_cost, new_stoplist, (EAST_pu, LAST_pu, EAST_do, LAST_do) + pythonic_solution = MinimalPassengerTravelTimeDispatcher()( + request, stoplist, space, seat_capacity + ) + py_min_cost, _, py_timewindows = pythonic_solution + tock = time() + print( + f"Computing insertion into {len_stoplist}-element stoplist with pure pythonic dispatcher took: {tock - tick} seconds" + ) + + # then call the cythonic dispatcher + request = cyds.TransportationRequest( + request_id=100, + creation_timestamp=1, + origin=origin, + destination=destination, + pickup_timewindow_min=0, + pickup_timewindow_max=inf, + delivery_timewindow_min=0, + delivery_timewindow_max=inf, + ) + + # Note: we need to create a Cythonic stoplist object here because we cannot pass a python list to + # CyMinimalPassengerTravelTimeDispatcher + space = cyspaces.Euclidean2D() + stoplist = stoplist_from_properties( + stoplist_properties=stoplist_properties, kind="cython", space=space + ) + tick = time() + # vehicle_id, new_stoplist, (min_cost, EAST_pu, LAST_pu, EAST_do, LAST_do) + cythonic_solution = CyMinimalPassengerTravelTimeDispatcher(LocType.R2LOC)( + request, stoplist, space, seat_capacity + ) + cy_min_cost, _, cy_timewindows = cythonic_solution + tock = time() + print( + f"Computing insertion into {len_stoplist}-element stoplist with cythonic dispatcher took: {tock-tick} seconds" + ) + + assert np.isclose(py_min_cost, cy_min_cost) + assert np.allclose(py_timewindows, cy_timewindows) + + +def test_equivalence_simulator_cython_and_python_minimal_passenger_travel_time_dispatcher( + seed=42, +): + """ + Tests that the simulation runs with pure pythonic and cythonic minimal passenger travel time dispatcher produces identical events. + """ + for py_space, cy_space in ( + (pyspaces.Euclidean2D(), cyspaces.Euclidean2D()), + # DO NOT test graph spaces for now, as we use different methods for computing the shortest path + # (floyd-warshall in Python and dijkstra in C++. Therefore differences in interpolation arise.) + # ( + # pyspaces.Graph.from_nx(make_nx_grid()), + # cyspaces.Graph.from_nx(make_nx_grid()), + # ), + ): + n_reqs = 100 + + random.seed(seed) + init_loc = py_space.random_point() + random.seed(seed) + assert init_loc == cy_space.random_point() + + ###################################################### + # PYTHON + ###################################################### + + ssfs = SlowSimpleFleetState( + initial_locations={7: init_loc}, + seat_capacities=10, + space=py_space, + dispatcher=MinimalPassengerTravelTimeDispatcher(), + vehicle_state_class=py_VehicleState, + ) + rg = RandomRequestGenerator( + space=py_space, + request_class=pyds.TransportationRequest, + seed=seed, + rate=1.5, + ) + py_reqs = list(it.islice(rg, n_reqs)) + py_events = list(ssfs.simulate(py_reqs)) + + ###################################################### + # CYTHON + ###################################################### + + ssfs = SlowSimpleFleetState( + initial_locations={7: init_loc}, + seat_capacities=10, + space=cy_space, + dispatcher=CMinimalPassengerTravelTimeDispatcher( + loc_type=cy_space.loc_type + ), + vehicle_state_class=cy_VehicleState, + ) + rg = RandomRequestGenerator( + space=cy_space, + request_class=cyds.TransportationRequest, + seed=seed, + rate=1.5, + ) + cy_reqs = list(it.islice(rg, n_reqs)) + cy_events = list(ssfs.simulate(cy_reqs)) + + ###################################################### + # COMPARE + ###################################################### + # assert that the returned events are the same + assert len(cy_events) == len(py_events) + for num, (cev, pev) in enumerate(zip(cy_events, py_events)): + assert type(cev) == type(pev) + assert np.allclose( + list(flatten([v for k, v in pev.items() if k != "event_type"])), + list(flatten([v for k, v in cev.items() if k != "event_type"])), + rtol=1e-4, + ) + + +def test_sanity_in_graph(): + """ + Insert a request, note delivery time. + Handle more requests so that there's no pooling. + Assert that the delivery time is not changed. + + Or more simply, assert that the vehicle moves at either the space's velocity or 0. + """ + + for velocity in [0.9, 1, 1.1]: + space = cyspaces.Graph.from_nx(make_nx_grid(), velocity=velocity) + # max_pickup_delay=0, + rg = RandomRequestGenerator( + rate=10, + space=space, + max_delivery_delay_abs=0, + request_class=cyds.TransportationRequest, + ) + + transportation_requests = list(it.islice(rg, 1000)) + + fs = SlowSimpleFleetState( + initial_locations={k: 0 for k in range(50)}, + seat_capacities=10, + space=space, + dispatcher=CMinimalPassengerTravelTimeDispatcher(LocType.INT), + vehicle_state_class=cy_VehicleState, + ) + + events = list(fs.simulate(transportation_requests)) + + rejections = set( + ev["request_id"] + for ev in events + if ev["event_type"] == "RequestRejectionEvent" + ) + pickup_times = { + ev["request_id"]: ev["timestamp"] + for ev in events + if ev["event_type"] == "PickupEvent" + } + delivery_times = { + ev["request_id"]: ev["timestamp"] + for ev in events + if ev["event_type"] == "DeliveryEvent" + } + + for req in transportation_requests: + if (rid := req.request_id) not in rejections: + assert isclose(req.delivery_timewindow_max, delivery_times[rid]) + assert isclose( + delivery_times[rid] - pickup_times[rid], + space.t(req.origin, req.destination), + ) + + +if __name__ == "__main__": + pytest.main(args=[__file__])