diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 78a7c0cd..00000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index 1e17640c..364453be 100644 --- a/.gitignore +++ b/.gitignore @@ -20,8 +20,9 @@ data/networks/* data/networks/example_network/ff/* data/networks/example_network/ch_graph/* data/networks/example_network/base/*.graph -data/pubtrans/* -!data/pubtrans/route_193 +data/pt/* +!data/pt/route_193 +!data/pt/example_network data/vehicles/* !data/vehicles/default_vehtype.csv !data/vehicles/low_range_vehtype.csv @@ -35,7 +36,8 @@ data/zones/* studies/* !studies/example_study studies/example_study/results/* -!studies/module_tests/* +!studies/module_tests +studies/module_tests/benchmark_comparison.csv studies/module_tests/results/* !studies/module_tests/results/benchmark.csv !studies/manhattan_case_study @@ -46,15 +48,18 @@ studies/chicago_case_study/results/* studies/munich_case_study/results/* # system dependent C++ Router files -src/routing/cpp_router/*.pyd -src/routing/cpp_router/build/* -src/routing/cpp_router/PyNetwork.cpp -src/routing/extractors/cache/* +src/routing/road/cpp_router/*.pyd +src/routing/road/cpp_router/build/* +src/routing/road/cpp_router/PyNetwork.cpp +src/routing/road/extractors/cache/* + +src/routing/pt/cpp_raptor_router/PyPTRouter.cpp +src/routing/pt/cpp_raptor_router/build/* # IDE-related files *.idea .vscode/* -src/routing/cpp_router/.vs/* +src/routing/road/cpp_router/.vs/* # Byte-compiled / optimized / DLL files __pycache__/ @@ -186,3 +191,9 @@ dmypy.json # Pyre type checker .pyre/ + +# MacOS +.DS_Store + +# GTFS preprocessing file +!src/preprocessing/pt/PTRouterGTFSPreperation.ipynb diff --git a/README.md b/README.md index a53ad9ba..aefe2e98 100644 --- a/README.md +++ b/README.md @@ -66,10 +66,17 @@ conda activate fleetpy ### 2️⃣ Install C++ Router (Recommended) -For improved routing efficiency, compile the C++ router: +For improved road network routing efficiency, compile the C++ road router: ```bash -cd FleetPy/src/routing/cpp_router +cd FleetPy/src/routing/road/cpp_router +python setup.py build_ext --inplace +``` + +To enable public transport routing, compile the C++ RAPTOR router: + +```bash +cd FleetPy/src/routing/pt/cpp_raptor_router python setup.py build_ext --inplace ``` diff --git a/changelog.md b/changelog.md index 306d9a82..9dc3f26f 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,91 @@ All notable changes to this project will be documented in this file. +## [1.1.0] - 2025-12-18 + +Key update: +1. Refactored routing file structure: road-related routing modules are moved to the `road` subdirectory, and PT-related routing modules are moved to the `pt` subdirectory. +2. Introduced a C++ routing module based on the RAPTOR algorithm for querying the fastest PT travel plans between two stations. +3. Introduced the PTControl module to simulate PT operator behavior, such as recording offer information and dynamically updating GTFS files. +4. Introduced three PTBroker strategy variants to simulate different levels of MaaS–DRT coordination for intermodal requests: Plan-As-You-Go (PTBrokerPAYG), Estimation-based Integration (PTBrokerEI), and Collaborative Coordination (PTBroker). +5. Introduced subrequest ID coding rules for intermodal scenarios, using unique integers to classify legs and `{parent_rid}_{subtrip_id}` to define new subrequest IDs. + +### Added +- cpp_raptor_router: C++ implementation of the PT router based on the RAPTOR algorithm + +- RaptorRouterCpp: Python entry point for the PT router, responsible for activating the cpp_raptor_router instance and regularizing PT requests + +- PTControlBase & PTControlBasic: Modules to simulate PT operator behavior, such as recording offer information and dynamically updating GTFS files + +- PTOffer: Class for recording PT offer information + +- IntermodalOffer: Class for recording offer information for intermodal requests + +- BasicIntermodalRequest: Class to simulate travel behavior of travelers with intermodal requests + +- example_100_intermodal.csv: Intermodal demand based on example_100.csv, containing 25 monomodal, 25 first-mile, 25 last-mile, and 25 first-last-mile requests + +- example_100_intermodal_lmwt30.csv: Variant of the intermodal demand file with a 30-second last-mile wait time constraint + +- example_gtfs: Public transport design based on example_network + +- PTBrokerBasic: Base class providing shared infrastructure for intermodal request handling across all PTBroker variants (FM, LM, FLM sub-request creation, offer assembly, booking confirmation) + +- PTBroker (Collaborative Coordination): Simulates a future scenario with tight MaaS–DRT integration. DRT provides a predicted FM dropoff time; PT feeds back the user's expected station waiting time, which MaaS uses to dynamically adjust the DRT dropoff deadline, giving DRT more pooling flexibility while guaranteeing PT connection. LM DRT wait time can also be constrained to minimize destination wait. + +- PTBrokerEI (Estimation-based Integration): Simulates current MaaS platforms with limited real-time DRT communication. FM dropoff time is estimated using `broker_maas_detour_time_factor` rather than obtained from an actual DRT offer. A conservative factor ensures PT is caught but increases travel time; an optimistic factor risks missing PT. + +- PTBrokerPAYG (Plan-As-You-Go): Simulates the absence of a MaaS platform. Each leg is planned only after the previous one completes (FM DRT → PT → LM DRT). Trips may be interrupted if a subsequent leg is unavailable. + +- intermodal_evaluation: Evaluation methods designed for intermodal scenarios + +- example_study & module_tests: Added intermodal scenario example experiments + +- globals: Added `RQ_MODAL_STATE`, `RQ_SUB_TRIP_ID`, and `PAYG_TRIP_STATE` enums for intermodal sub-request classification; added global variable names for PT (`G_PT_*`), intermodal offers (`G_IM_*`), and broker configuration (`G_BROKER_*`, including `G_BROKER_MAAS_DETOUR_TIME_FACTOR`, `G_BROKER_TRANSFER_SEARCH_METHOD`, `G_BROKER_ALWAYS_QUERY_PT`, `G_IM_LM_WAIT_TIME`) + +- init_modules: Added initialization code for PTControl and PTBroker modules + +- FleetSimulationBase: Added code to load PTControl and PTBroker modules + +- PlanRequest: Added `set_new_dropoff_time_constraint` method to update the passenger's latest drop-off time constraint + +- Demand: Added `create_sub_requests` method to establish sub-requests for corresponding legs of intermodal requests + +- RequestBase: Added `modal state` attribute (default: monomodal) and `get_modal_state` method + +- PTRouterGTFSPreperation: Jupyter notebook for cleaning and formatting raw GTFS data for RaptorRouterCpp + +### Changed +- BrokerBase & BrokerBasic: `collect_offers` method now accepts an input variable `sim_time` (int, default: None) + +- ImmediateDecisionsSimulation: Added `sim_time` input variable when calling `self.broker.collect_offers` + +- RequestBase: Updated `create_SubTripRequest` method to create intermodal sub-requests + +- insertion: `insertion_with_heuristics` and `reservation_insertion_with_heuristics` methods added `excluded_vid` input (list of vehicle IDs that should not be considered for assignment) + +- RollingHorizon: `return_immediate_reservation_offer` method added `excluded_vid` input + +- PoolingIRSOnly: `user_request` method added optional `max_wait_time` parameter (used for LM leg of intermodal requests); tracks `flm_excluded_vid` to exclude the FM vehicle from LM assignment in FLM requests + +- Vehicles: `assign_vehicle_plan` method now uses `rid_struct` to obtain request information + +- FleetSimulationBase: Modified public transportation module loading code; modified `evaluate` method to use `G_EVAL_METHOD` to specify standard result evaluation (default: standard_evaluation) + +- gitignore: Ignored specific C++ Router files + +### Deprecated +- globals: Traveler modal state global variables are no longer used (G_RQ_STATE_MONOMODAL, G_RQ_STATE_FIRSTMILE, G_RQ_STATE_LASTMILE, G_RQ_STATE_FIRSTLASTMILE) + +### Removed +- globals: Traveler modal state global variable names + +### Fixed +- RollingHorizon: Correctly retrieve `vid` and `veh_obj` in `user_cancels_request` method + +- FleetSimulationBase: In `update_sim_state_fleets`, ensured `rid_struct` is the actual key of the dictionary returned by `veh_obj.update_veh_state` + + ## [1.0.0] - 2025-04-DD diff --git a/data/demand/example_demand/matched/example_network/example_100_intermodal.csv b/data/demand/example_demand/matched/example_network/example_100_intermodal.csv new file mode 100644 index 00000000..05314130 --- /dev/null +++ b/data/demand/example_demand/matched/example_network/example_100_intermodal.csv @@ -0,0 +1,101 @@ +rq_time,start,end,request_id,is_multimodal,modal_state_value,transfer_station_ids,max_transfers +194,2966,2977,0,1,2,s15,1 +217,2966,2973,1,0,0,-1,1 +301,2976,2966,2,1,1,s15,1 +388,2982,2980,3,0,0,-1,1 +397,2977,2968,4,1,3,s15;s1,1 +679,2966,2985,5,1,2,s15,1 +890,2993,2988,6,0,0,-1,1 +896,2966,2973,7,1,2,s15,1 +933,2977,2986,8,1,3,s15;s1,1 +959,2976,2982,9,0,0,-1,1 +983,2977,2966,10,1,1,s15,1 +1085,2966,2981,11,1,2,s15,1 +1092,2992,2980,12,0,0,-1,1 +1185,2987,2984,13,0,0,-1,1 +1204,2966,2981,14,1,2,s15,1 +1320,2993,2980,15,1,1,s1,1 +1449,2977,2967,16,1,3,s15;s1,1 +1511,2984,2966,17,1,1,s15,1 +1514,2989,2977,18,1,3,s1;s15,1 +1558,2989,2975,19,1,3,s1;s15,1 +1587,2981,2979,20,0,0,-1,1 +1659,2978,2966,21,1,1,s15,1 +1668,2966,2982,22,1,2,s15,1 +1681,2967,2980,23,1,1,s1,1 +1725,2966,2977,24,1,2,s15,1 +1735,2990,2982,25,1,3,s1;s15,1 +2045,2980,2993,26,1,2,s1,1 +2045,2969,2993,27,0,0,-1,1 +2127,2973,2966,28,1,1,s15,1 +2198,2966,2976,29,1,2,s15,1 +2309,2986,2966,30,1,1,s15,1 +2383,2989,2978,31,1,3,s1;s15,1 +2416,2981,2969,32,1,3,s15;s1,1 +2494,2992,2980,33,1,1,s1,1 +2540,2980,2970,34,1,2,s1,1 +2626,2990,2988,35,1,3,s1;s15,1 +2727,2972,2985,36,0,0,-1,1 +2803,2970,2972,37,0,0,-1,1 +2881,2993,2980,38,1,1,s1,1 +2912,2992,2982,39,1,3,s1;s15,1 +2947,2990,2981,40,0,0,-1,1 +3008,2966,2982,41,1,2,s15,1 +3064,2986,2970,42,0,0,-1,1 +3088,2973,2966,43,1,1,s15,1 +3109,2967,2981,44,1,3,s1;s15,1 +3303,2980,2971,45,1,2,s1,1 +3373,2976,2987,46,0,0,-1,1 +3437,2971,2966,47,1,1,s15,1 +3616,2966,2982,48,1,2,s15,1 +3637,2983,2966,49,1,1,s15,1 +3725,2974,2966,50,1,1,s15,1 +3846,2966,2988,51,1,2,s15,1 +3855,2969,2979,52,1,3,s1;s15,1 +3881,2980,2990,53,1,2,s1,1 +4260,2980,2992,54,1,2,s1,1 +4325,2972,2991,55,0,0,-1,1 +4341,2979,2969,56,1,3,s15;s1,1 +4394,2983,2967,57,1,3,s15;s1,1 +4464,2981,2971,58,0,0,-1,1 +4476,2966,2981,59,1,2,s15,1 +4564,2980,2969,60,1,2,s1,1 +4621,2979,2968,61,1,3,s15;s1,1 +4642,2991,2980,62,1,1,s1,1 +4722,2989,2967,63,0,0,-1,1 +4877,2978,2992,64,1,3,s15;s1,1 +4944,2980,2972,65,1,2,s1,1 +4960,2979,2966,66,1,1,s15,1 +4984,2987,2966,67,1,1,s15,1 +4997,2980,2992,68,1,2,s1,1 +5118,2968,2980,69,1,1,s1,1 +5177,2993,2977,70,1,3,s1;s15,1 +5237,2985,2967,71,1,3,s15;s1,1 +5401,2967,2981,72,0,0,-1,1 +5459,2980,2969,73,1,2,s1,1 +5470,2969,2979,74,0,0,-1,1 +5486,2984,2993,75,1,3,s15;s1,1 +5505,2984,2992,76,1,3,s15;s1,1 +5531,2986,2966,77,1,1,s15,1 +5541,2970,2981,78,1,3,s1;s15,1 +5563,2978,2975,79,0,0,-1,1 +5571,2980,2993,80,1,2,s1,1 +5580,2984,2969,81,0,0,-1,1 +5622,2970,2980,82,1,1,s1,1 +5637,2975,2976,83,0,0,-1,1 +5701,2966,2987,84,1,2,s15,1 +5726,2971,2978,85,0,0,-1,1 +5788,2969,2982,86,1,3,s1;s15,1 +6062,2978,2993,87,1,3,s15;s1,1 +6252,2966,2982,88,1,2,s15,1 +6437,2992,2980,89,0,0,-1,1 +6580,2982,2966,90,1,1,s15,1 +6617,2973,2982,91,0,0,-1,1 +6648,2973,2966,92,1,1,s15,1 +6685,2967,2974,93,1,3,s1;s15,1 +6707,2968,2991,94,0,0,-1,1 +6850,2966,2987,95,1,2,s15,1 +6886,2991,2980,96,1,1,s1,1 +6910,2983,2993,97,1,3,s15;s1,1 +6944,2976,2966,98,1,1,s15,1 +7152,2988,2980,99,1,1,s1,1 diff --git a/data/demand/example_demand/matched/example_network/example_100_intermodal_lmwt30.csv b/data/demand/example_demand/matched/example_network/example_100_intermodal_lmwt30.csv new file mode 100644 index 00000000..91f60a81 --- /dev/null +++ b/data/demand/example_demand/matched/example_network/example_100_intermodal_lmwt30.csv @@ -0,0 +1,101 @@ +rq_time,start,end,request_id,is_multimodal,modal_state_value,transfer_station_ids,max_transfers,im_lastmile_wait_time +194,2966,2977,0,1,2,s15,1,30.0 +217,2966,2973,1,0,0,-1,1, +301,2976,2966,2,1,1,s15,1, +388,2982,2980,3,0,0,-1,1, +397,2977,2968,4,1,3,s15;s1,1,30.0 +679,2966,2985,5,1,2,s15,1,30.0 +890,2993,2988,6,0,0,-1,1, +896,2966,2973,7,1,2,s15,1,30.0 +933,2977,2986,8,1,3,s15;s1,1,30.0 +959,2976,2982,9,0,0,-1,1, +983,2977,2966,10,1,1,s15,1, +1085,2966,2981,11,1,2,s15,1,30.0 +1092,2992,2980,12,0,0,-1,1, +1185,2987,2984,13,0,0,-1,1, +1204,2966,2981,14,1,2,s15,1,30.0 +1320,2993,2980,15,1,1,s1,1, +1449,2977,2967,16,1,3,s15;s1,1,30.0 +1511,2984,2966,17,1,1,s15,1, +1514,2989,2977,18,1,3,s1;s15,1,30.0 +1558,2989,2975,19,1,3,s1;s15,1,30.0 +1587,2981,2979,20,0,0,-1,1, +1659,2978,2966,21,1,1,s15,1, +1668,2966,2982,22,1,2,s15,1,30.0 +1681,2967,2980,23,1,1,s1,1, +1725,2966,2977,24,1,2,s15,1,30.0 +1735,2990,2982,25,1,3,s1;s15,1,30.0 +2045,2980,2993,26,1,2,s1,1,30.0 +2045,2969,2993,27,0,0,-1,1, +2127,2973,2966,28,1,1,s15,1, +2198,2966,2976,29,1,2,s15,1,30.0 +2309,2986,2966,30,1,1,s15,1, +2383,2989,2978,31,1,3,s1;s15,1,30.0 +2416,2981,2969,32,1,3,s15;s1,1,30.0 +2494,2992,2980,33,1,1,s1,1, +2540,2980,2970,34,1,2,s1,1,30.0 +2626,2990,2988,35,1,3,s1;s15,1,30.0 +2727,2972,2985,36,0,0,-1,1, +2803,2970,2972,37,0,0,-1,1, +2881,2993,2980,38,1,1,s1,1, +2912,2992,2982,39,1,3,s1;s15,1,30.0 +2947,2990,2981,40,0,0,-1,1, +3008,2966,2982,41,1,2,s15,1,30.0 +3064,2986,2970,42,0,0,-1,1, +3088,2973,2966,43,1,1,s15,1, +3109,2967,2981,44,1,3,s1;s15,1,30.0 +3303,2980,2971,45,1,2,s1,1,30.0 +3373,2976,2987,46,0,0,-1,1, +3437,2971,2966,47,1,1,s15,1, +3616,2966,2982,48,1,2,s15,1,30.0 +3637,2983,2966,49,1,1,s15,1, +3725,2974,2966,50,1,1,s15,1, +3846,2966,2988,51,1,2,s15,1,30.0 +3855,2969,2979,52,1,3,s1;s15,1,30.0 +3881,2980,2990,53,1,2,s1,1,30.0 +4260,2980,2992,54,1,2,s1,1,30.0 +4325,2972,2991,55,0,0,-1,1, +4341,2979,2969,56,1,3,s15;s1,1,30.0 +4394,2983,2967,57,1,3,s15;s1,1,30.0 +4464,2981,2971,58,0,0,-1,1, +4476,2966,2981,59,1,2,s15,1,30.0 +4564,2980,2969,60,1,2,s1,1,30.0 +4621,2979,2968,61,1,3,s15;s1,1,30.0 +4642,2991,2980,62,1,1,s1,1, +4722,2989,2967,63,0,0,-1,1, +4877,2978,2992,64,1,3,s15;s1,1,30.0 +4944,2980,2972,65,1,2,s1,1,30.0 +4960,2979,2966,66,1,1,s15,1, +4984,2987,2966,67,1,1,s15,1, +4997,2980,2992,68,1,2,s1,1,30.0 +5118,2968,2980,69,1,1,s1,1, +5177,2993,2977,70,1,3,s1;s15,1,30.0 +5237,2985,2967,71,1,3,s15;s1,1,30.0 +5401,2967,2981,72,0,0,-1,1, +5459,2980,2969,73,1,2,s1,1,30.0 +5470,2969,2979,74,0,0,-1,1, +5486,2984,2993,75,1,3,s15;s1,1,30.0 +5505,2984,2992,76,1,3,s15;s1,1,30.0 +5531,2986,2966,77,1,1,s15,1, +5541,2970,2981,78,1,3,s1;s15,1,30.0 +5563,2978,2975,79,0,0,-1,1, +5571,2980,2993,80,1,2,s1,1,30.0 +5580,2984,2969,81,0,0,-1,1, +5622,2970,2980,82,1,1,s1,1, +5637,2975,2976,83,0,0,-1,1, +5701,2966,2987,84,1,2,s15,1,30.0 +5726,2971,2978,85,0,0,-1,1, +5788,2969,2982,86,1,3,s1;s15,1,30.0 +6062,2978,2993,87,1,3,s15;s1,1,30.0 +6252,2966,2982,88,1,2,s15,1,30.0 +6437,2992,2980,89,0,0,-1,1, +6580,2982,2966,90,1,1,s15,1, +6617,2973,2982,91,0,0,-1,1, +6648,2973,2966,92,1,1,s15,1, +6685,2967,2974,93,1,3,s1;s15,1,30.0 +6707,2968,2991,94,0,0,-1,1, +6850,2966,2987,95,1,2,s15,1,30.0 +6886,2991,2980,96,1,1,s1,1, +6910,2983,2993,97,1,3,s15;s1,1,30.0 +6944,2976,2966,98,1,1,s15,1, +7152,2988,2980,99,1,1,s1,1, diff --git a/data/networks/osm_route_MVG_road/base/crs.info b/data/networks/osm_route_MVG_road/base/crs.info old mode 100755 new mode 100644 diff --git a/data/pt/example_network/example_gtfs/matched/agency_fp.txt b/data/pt/example_network/example_gtfs/matched/agency_fp.txt new file mode 100644 index 00000000..d51e0d61 --- /dev/null +++ b/data/pt/example_network/example_gtfs/matched/agency_fp.txt @@ -0,0 +1,2 @@ +agency_id,agency_name +pt,FleetPy PT Operator diff --git a/data/pt/example_network/example_gtfs/matched/calendar_fp.txt b/data/pt/example_network/example_gtfs/matched/calendar_fp.txt new file mode 100644 index 00000000..6acf3d08 --- /dev/null +++ b/data/pt/example_network/example_gtfs/matched/calendar_fp.txt @@ -0,0 +1,2 @@ +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +0,1,1,1,1,1,1,1,20000101,20991231 \ No newline at end of file diff --git a/data/pt/example_network/example_gtfs/matched/routes_fp.txt b/data/pt/example_network/example_gtfs/matched/routes_fp.txt new file mode 100644 index 00000000..f34cea08 --- /dev/null +++ b/data/pt/example_network/example_gtfs/matched/routes_fp.txt @@ -0,0 +1,3 @@ +route_id,route_short_name,route_desc +196,196,Bus +U5,U5,U-Bahn \ No newline at end of file diff --git a/data/pt/example_network/example_gtfs/matched/stations_fp.txt b/data/pt/example_network/example_gtfs/matched/stations_fp.txt new file mode 100644 index 00000000..1c3025c8 --- /dev/null +++ b/data/pt/example_network/example_gtfs/matched/stations_fp.txt @@ -0,0 +1,18 @@ +station_id,station_name,station_lat,station_lon,stops_included,station_stop_transfer_times,num_stops_included +s1,Neuperlach Zentrum,48.1011253,11.64650495,"['1-0', '1-1', '1-2', '1-3']","[1, 1, 1, 1]",4 +s2,Jakob-Kaiser-Straße,48.10435272,11.64040539,"['2-0', '2-1']","[1, 1]",2 +s3,Holzwiesenstraße,48.10307497,11.63659653,"['3-0', '3-1']","[1, 1]",2 +s4,Wilhelm-Hoegner-Straße,48.09887559,11.6363989,"['4-0', '4-1']","[1, 1]",2 +s5,Wolframstraße,48.09486185,11.63579703,"['5-0', '5-1']","[1, 1]",2 +s6,Perlach Bahnhof,48.09355387,11.63056883,"['6-0', '6-1']","[1, 1]",2 +s7,Weidener Straße,48.09252186,11.62810745,"['7-0', '7-1']","[1, 1]",2 +s8,Bayerwaldstraße,48.08847762,11.62821525,"['8-0', '8-1']","[1, 1]",2 +s9,Nailastraße,48.08860963,11.63358717,"['9-0', '9-1']","[1, 1]",2 +s10,Rudolf-Zorn-Straße,48.0908898,11.63481786,"['10-0', '10-1']","[1, 1]",2 +s11,Hermann-Pünder-Straße,48.08844762,11.63584194,"['11-0', '11-1']","[1, 1]",2 +s12,Ludwig-Linsert-Straße,48.08627536,11.63483583,"['12-0', '12-1']","[1, 1]",2 +s13,Ludwig-Erhard-Allee,48.0852372,11.63944419,"['13-0', '13-1']","[1, 1]",2 +s14,Curd-Jürgens-Straße,48.08536922,11.64292067,"['14-0', '14-1']","[1, 1]",2 +s15,Neuperlach Süd,48.0893717,11.6446095,"['15-0', '15-1', '15-2', '15-3']","[1, 1, 1, 1]",4 +s16,Therese-Giehse-Allee,48.09477185,11.64277694,"['16-2', '16-3']","[1, 1]",2 +s17,Thomas-Dehler-Straße,48.09735172,11.64436696,"['17-2', '17-3']","[1, 1]",2 \ No newline at end of file diff --git a/data/pt/example_network/example_gtfs/matched/stop_times_fp.txt b/data/pt/example_network/example_gtfs/matched/stop_times_fp.txt new file mode 100644 index 00000000..f03aa90f --- /dev/null +++ b/data/pt/example_network/example_gtfs/matched/stop_times_fp.txt @@ -0,0 +1,559 @@ +trip_id,arrival_time,departure_time,stop_id,stop_sequence +196-0-0,0:00:00,0:00:00,1-0,1 +196-0-0,0:02:00,0:02:00,2-0,2 +196-0-0,0:03:30,0:03:30,3-0,3 +196-0-0,0:04:30,0:04:30,4-0,4 +196-0-0,0:05:30,0:05:30,5-0,5 +196-0-0,0:06:30,0:06:30,6-0,6 +196-0-0,0:07:30,0:07:30,7-0,7 +196-0-0,0:09:30,0:09:30,8-0,8 +196-0-0,0:10:30,0:10:30,9-0,9 +196-0-0,0:12:30,0:12:30,10-0,10 +196-0-0,0:14:30,0:14:30,11-0,11 +196-0-0,0:15:30,0:15:30,12-0,12 +196-0-0,0:17:00,0:17:00,13-0,13 +196-0-0,0:18:00,0:18:00,14-0,14 +196-0-0,0:20:00,0:20:00,15-0,15 +196-1-0,0:20:00,0:20:00,1-0,1 +196-1-0,0:22:00,0:22:00,2-0,2 +196-1-0,0:23:30,0:23:30,3-0,3 +196-1-0,0:24:30,0:24:30,4-0,4 +196-1-0,0:25:30,0:25:30,5-0,5 +196-1-0,0:26:30,0:26:30,6-0,6 +196-1-0,0:27:30,0:27:30,7-0,7 +196-1-0,0:29:30,0:29:30,8-0,8 +196-1-0,0:30:30,0:30:30,9-0,9 +196-1-0,0:32:30,0:32:30,10-0,10 +196-1-0,0:34:30,0:34:30,11-0,11 +196-1-0,0:35:30,0:35:30,12-0,12 +196-1-0,0:37:00,0:37:00,13-0,13 +196-1-0,0:38:00,0:38:00,14-0,14 +196-1-0,0:40:00,0:40:00,15-0,15 +196-2-0,0:40:00,0:40:00,1-0,1 +196-2-0,0:42:00,0:42:00,2-0,2 +196-2-0,0:43:30,0:43:30,3-0,3 +196-2-0,0:44:30,0:44:30,4-0,4 +196-2-0,0:45:30,0:45:30,5-0,5 +196-2-0,0:46:30,0:46:30,6-0,6 +196-2-0,0:47:30,0:47:30,7-0,7 +196-2-0,0:49:30,0:49:30,8-0,8 +196-2-0,0:50:30,0:50:30,9-0,9 +196-2-0,0:52:30,0:52:30,10-0,10 +196-2-0,0:54:30,0:54:30,11-0,11 +196-2-0,0:55:30,0:55:30,12-0,12 +196-2-0,0:57:00,0:57:00,13-0,13 +196-2-0,0:58:00,0:58:00,14-0,14 +196-2-0,1:00:00,1:00:00,15-0,15 +196-3-0,1:00:00,1:00:00,1-0,1 +196-3-0,1:02:00,1:02:00,2-0,2 +196-3-0,1:03:30,1:03:30,3-0,3 +196-3-0,1:04:30,1:04:30,4-0,4 +196-3-0,1:05:30,1:05:30,5-0,5 +196-3-0,1:06:30,1:06:30,6-0,6 +196-3-0,1:07:30,1:07:30,7-0,7 +196-3-0,1:09:30,1:09:30,8-0,8 +196-3-0,1:10:30,1:10:30,9-0,9 +196-3-0,1:12:30,1:12:30,10-0,10 +196-3-0,1:14:30,1:14:30,11-0,11 +196-3-0,1:15:30,1:15:30,12-0,12 +196-3-0,1:17:00,1:17:00,13-0,13 +196-3-0,1:18:00,1:18:00,14-0,14 +196-3-0,1:20:00,1:20:00,15-0,15 +196-4-0,1:20:00,1:20:00,1-0,1 +196-4-0,1:22:00,1:22:00,2-0,2 +196-4-0,1:23:30,1:23:30,3-0,3 +196-4-0,1:24:30,1:24:30,4-0,4 +196-4-0,1:25:30,1:25:30,5-0,5 +196-4-0,1:26:30,1:26:30,6-0,6 +196-4-0,1:27:30,1:27:30,7-0,7 +196-4-0,1:29:30,1:29:30,8-0,8 +196-4-0,1:30:30,1:30:30,9-0,9 +196-4-0,1:32:30,1:32:30,10-0,10 +196-4-0,1:34:30,1:34:30,11-0,11 +196-4-0,1:35:30,1:35:30,12-0,12 +196-4-0,1:37:00,1:37:00,13-0,13 +196-4-0,1:38:00,1:38:00,14-0,14 +196-4-0,1:40:00,1:40:00,15-0,15 +196-5-0,1:40:00,1:40:00,1-0,1 +196-5-0,1:42:00,1:42:00,2-0,2 +196-5-0,1:43:30,1:43:30,3-0,3 +196-5-0,1:44:30,1:44:30,4-0,4 +196-5-0,1:45:30,1:45:30,5-0,5 +196-5-0,1:46:30,1:46:30,6-0,6 +196-5-0,1:47:30,1:47:30,7-0,7 +196-5-0,1:49:30,1:49:30,8-0,8 +196-5-0,1:50:30,1:50:30,9-0,9 +196-5-0,1:52:30,1:52:30,10-0,10 +196-5-0,1:54:30,1:54:30,11-0,11 +196-5-0,1:55:30,1:55:30,12-0,12 +196-5-0,1:57:00,1:57:00,13-0,13 +196-5-0,1:58:00,1:58:00,14-0,14 +196-5-0,2:00:00,2:00:00,15-0,15 +196-6-0,2:00:00,2:00:00,1-0,1 +196-6-0,2:02:00,2:02:00,2-0,2 +196-6-0,2:03:30,2:03:30,3-0,3 +196-6-0,2:04:30,2:04:30,4-0,4 +196-6-0,2:05:30,2:05:30,5-0,5 +196-6-0,2:06:30,2:06:30,6-0,6 +196-6-0,2:07:30,2:07:30,7-0,7 +196-6-0,2:09:30,2:09:30,8-0,8 +196-6-0,2:10:30,2:10:30,9-0,9 +196-6-0,2:12:30,2:12:30,10-0,10 +196-6-0,2:14:30,2:14:30,11-0,11 +196-6-0,2:15:30,2:15:30,12-0,12 +196-6-0,2:17:00,2:17:00,13-0,13 +196-6-0,2:18:00,2:18:00,14-0,14 +196-6-0,2:20:00,2:20:00,15-0,15 +196-7-0,2:20:00,2:20:00,1-0,1 +196-7-0,2:22:00,2:22:00,2-0,2 +196-7-0,2:23:30,2:23:30,3-0,3 +196-7-0,2:24:30,2:24:30,4-0,4 +196-7-0,2:25:30,2:25:30,5-0,5 +196-7-0,2:26:30,2:26:30,6-0,6 +196-7-0,2:27:30,2:27:30,7-0,7 +196-7-0,2:29:30,2:29:30,8-0,8 +196-7-0,2:30:30,2:30:30,9-0,9 +196-7-0,2:32:30,2:32:30,10-0,10 +196-7-0,2:34:30,2:34:30,11-0,11 +196-7-0,2:35:30,2:35:30,12-0,12 +196-7-0,2:37:00,2:37:00,13-0,13 +196-7-0,2:38:00,2:38:00,14-0,14 +196-7-0,2:40:00,2:40:00,15-0,15 +196-8-0,2:40:00,2:40:00,1-0,1 +196-8-0,2:42:00,2:42:00,2-0,2 +196-8-0,2:43:30,2:43:30,3-0,3 +196-8-0,2:44:30,2:44:30,4-0,4 +196-8-0,2:45:30,2:45:30,5-0,5 +196-8-0,2:46:30,2:46:30,6-0,6 +196-8-0,2:47:30,2:47:30,7-0,7 +196-8-0,2:49:30,2:49:30,8-0,8 +196-8-0,2:50:30,2:50:30,9-0,9 +196-8-0,2:52:30,2:52:30,10-0,10 +196-8-0,2:54:30,2:54:30,11-0,11 +196-8-0,2:55:30,2:55:30,12-0,12 +196-8-0,2:57:00,2:57:00,13-0,13 +196-8-0,2:58:00,2:58:00,14-0,14 +196-8-0,3:00:00,3:00:00,15-0,15 +196-0-1,0:00:00,0:00:00,15-1,1 +196-0-1,0:02:00,0:02:00,14-1,2 +196-0-1,0:03:30,0:03:30,13-1,3 +196-0-1,0:04:30,0:04:30,12-1,4 +196-0-1,0:05:30,0:05:30,11-1,5 +196-0-1,0:06:30,0:06:30,10-1,6 +196-0-1,0:07:30,0:07:30,9-1,7 +196-0-1,0:09:30,0:09:30,8-1,8 +196-0-1,0:10:30,0:10:30,7-1,9 +196-0-1,0:12:30,0:12:30,6-1,10 +196-0-1,0:14:30,0:14:30,5-1,11 +196-0-1,0:15:30,0:15:30,4-1,12 +196-0-1,0:17:00,0:17:00,3-1,13 +196-0-1,0:18:00,0:18:00,2-1,14 +196-0-1,0:20:00,0:20:00,1-1,15 +196-1-1,0:20:00,0:20:00,15-1,1 +196-1-1,0:22:00,0:22:00,14-1,2 +196-1-1,0:23:30,0:23:30,13-1,3 +196-1-1,0:24:30,0:24:30,12-1,4 +196-1-1,0:25:30,0:25:30,11-1,5 +196-1-1,0:26:30,0:26:30,10-1,6 +196-1-1,0:27:30,0:27:30,9-1,7 +196-1-1,0:29:30,0:29:30,8-1,8 +196-1-1,0:30:30,0:30:30,7-1,9 +196-1-1,0:32:30,0:32:30,6-1,10 +196-1-1,0:34:30,0:34:30,5-1,11 +196-1-1,0:35:30,0:35:30,4-1,12 +196-1-1,0:37:00,0:37:00,3-1,13 +196-1-1,0:38:00,0:38:00,2-1,14 +196-1-1,0:40:00,0:40:00,1-1,15 +196-2-1,0:40:00,0:40:00,15-1,1 +196-2-1,0:42:00,0:42:00,14-1,2 +196-2-1,0:43:30,0:43:30,13-1,3 +196-2-1,0:44:30,0:44:30,12-1,4 +196-2-1,0:45:30,0:45:30,11-1,5 +196-2-1,0:46:30,0:46:30,10-1,6 +196-2-1,0:47:30,0:47:30,9-1,7 +196-2-1,0:49:30,0:49:30,8-1,8 +196-2-1,0:50:30,0:50:30,7-1,9 +196-2-1,0:52:30,0:52:30,6-1,10 +196-2-1,0:54:30,0:54:30,5-1,11 +196-2-1,0:55:30,0:55:30,4-1,12 +196-2-1,0:57:00,0:57:00,3-1,13 +196-2-1,0:58:00,0:58:00,2-1,14 +196-2-1,1:00:00,1:00:00,1-1,15 +196-3-1,1:00:00,1:00:00,15-1,1 +196-3-1,1:02:00,1:02:00,14-1,2 +196-3-1,1:03:30,1:03:30,13-1,3 +196-3-1,1:04:30,1:04:30,12-1,4 +196-3-1,1:05:30,1:05:30,11-1,5 +196-3-1,1:06:30,1:06:30,10-1,6 +196-3-1,1:07:30,1:07:30,9-1,7 +196-3-1,1:09:30,1:09:30,8-1,8 +196-3-1,1:10:30,1:10:30,7-1,9 +196-3-1,1:12:30,1:12:30,6-1,10 +196-3-1,1:14:30,1:14:30,5-1,11 +196-3-1,1:15:30,1:15:30,4-1,12 +196-3-1,1:17:00,1:17:00,3-1,13 +196-3-1,1:18:00,1:18:00,2-1,14 +196-3-1,1:20:00,1:20:00,1-1,15 +196-4-1,1:20:00,1:20:00,15-1,1 +196-4-1,1:22:00,1:22:00,14-1,2 +196-4-1,1:23:30,1:23:30,13-1,3 +196-4-1,1:24:30,1:24:30,12-1,4 +196-4-1,1:25:30,1:25:30,11-1,5 +196-4-1,1:26:30,1:26:30,10-1,6 +196-4-1,1:27:30,1:27:30,9-1,7 +196-4-1,1:29:30,1:29:30,8-1,8 +196-4-1,1:30:30,1:30:30,7-1,9 +196-4-1,1:32:30,1:32:30,6-1,10 +196-4-1,1:34:30,1:34:30,5-1,11 +196-4-1,1:35:30,1:35:30,4-1,12 +196-4-1,1:37:00,1:37:00,3-1,13 +196-4-1,1:38:00,1:38:00,2-1,14 +196-4-1,1:40:00,1:40:00,1-1,15 +196-5-1,1:40:00,1:40:00,15-1,1 +196-5-1,1:42:00,1:42:00,14-1,2 +196-5-1,1:43:30,1:43:30,13-1,3 +196-5-1,1:44:30,1:44:30,12-1,4 +196-5-1,1:45:30,1:45:30,11-1,5 +196-5-1,1:46:30,1:46:30,10-1,6 +196-5-1,1:47:30,1:47:30,9-1,7 +196-5-1,1:49:30,1:49:30,8-1,8 +196-5-1,1:50:30,1:50:30,7-1,9 +196-5-1,1:52:30,1:52:30,6-1,10 +196-5-1,1:54:30,1:54:30,5-1,11 +196-5-1,1:55:30,1:55:30,4-1,12 +196-5-1,1:57:00,1:57:00,3-1,13 +196-5-1,1:58:00,1:58:00,2-1,14 +196-5-1,2:00:00,2:00:00,1-1,15 +196-6-1,2:00:00,2:00:00,15-1,1 +196-6-1,2:02:00,2:02:00,14-1,2 +196-6-1,2:03:30,2:03:30,13-1,3 +196-6-1,2:04:30,2:04:30,12-1,4 +196-6-1,2:05:30,2:05:30,11-1,5 +196-6-1,2:06:30,2:06:30,10-1,6 +196-6-1,2:07:30,2:07:30,9-1,7 +196-6-1,2:09:30,2:09:30,8-1,8 +196-6-1,2:10:30,2:10:30,7-1,9 +196-6-1,2:12:30,2:12:30,6-1,10 +196-6-1,2:14:30,2:14:30,5-1,11 +196-6-1,2:15:30,2:15:30,4-1,12 +196-6-1,2:17:00,2:17:00,3-1,13 +196-6-1,2:18:00,2:18:00,2-1,14 +196-6-1,2:20:00,2:20:00,1-1,15 +196-7-1,2:20:00,2:20:00,15-1,1 +196-7-1,2:22:00,2:22:00,14-1,2 +196-7-1,2:23:30,2:23:30,13-1,3 +196-7-1,2:24:30,2:24:30,12-1,4 +196-7-1,2:25:30,2:25:30,11-1,5 +196-7-1,2:26:30,2:26:30,10-1,6 +196-7-1,2:27:30,2:27:30,9-1,7 +196-7-1,2:29:30,2:29:30,8-1,8 +196-7-1,2:30:30,2:30:30,7-1,9 +196-7-1,2:32:30,2:32:30,6-1,10 +196-7-1,2:34:30,2:34:30,5-1,11 +196-7-1,2:35:30,2:35:30,4-1,12 +196-7-1,2:37:00,2:37:00,3-1,13 +196-7-1,2:38:00,2:38:00,2-1,14 +196-7-1,2:40:00,2:40:00,1-1,15 +196-8-1,2:40:00,2:40:00,15-1,1 +196-8-1,2:42:00,2:42:00,14-1,2 +196-8-1,2:43:30,2:43:30,13-1,3 +196-8-1,2:44:30,2:44:30,12-1,4 +196-8-1,2:45:30,2:45:30,11-1,5 +196-8-1,2:46:30,2:46:30,10-1,6 +196-8-1,2:47:30,2:47:30,9-1,7 +196-8-1,2:49:30,2:49:30,8-1,8 +196-8-1,2:50:30,2:50:30,7-1,9 +196-8-1,2:52:30,2:52:30,6-1,10 +196-8-1,2:54:30,2:54:30,5-1,11 +196-8-1,2:55:30,2:55:30,4-1,12 +196-8-1,2:57:00,2:57:00,3-1,13 +196-8-1,2:58:00,2:58:00,2-1,14 +196-8-1,3:00:00,3:00:00,1-1,15 +U5-0-0,0:00:00,0:00:00,1-2,1 +U5-0-0,0:01:00,0:01:00,17-2,2 +U5-0-0,0:02:00,0:02:00,16-2,3 +U5-0-0,0:03:00,0:03:00,15-2,4 +U5-1-0,0:05:00,0:05:00,1-2,1 +U5-1-0,0:06:00,0:06:00,17-2,2 +U5-1-0,0:07:00,0:07:00,16-2,3 +U5-1-0,0:08:00,0:08:00,15-2,4 +U5-2-0,0:10:00,0:10:00,1-2,1 +U5-2-0,0:11:00,0:11:00,17-2,2 +U5-2-0,0:12:00,0:12:00,16-2,3 +U5-2-0,0:13:00,0:13:00,15-2,4 +U5-3-0,0:15:00,0:15:00,1-2,1 +U5-3-0,0:16:00,0:16:00,17-2,2 +U5-3-0,0:17:00,0:17:00,16-2,3 +U5-3-0,0:18:00,0:18:00,15-2,4 +U5-4-0,0:20:00,0:20:00,1-2,1 +U5-4-0,0:21:00,0:21:00,17-2,2 +U5-4-0,0:22:00,0:22:00,16-2,3 +U5-4-0,0:23:00,0:23:00,15-2,4 +U5-5-0,0:25:00,0:25:00,1-2,1 +U5-5-0,0:26:00,0:26:00,17-2,2 +U5-5-0,0:27:00,0:27:00,16-2,3 +U5-5-0,0:28:00,0:28:00,15-2,4 +U5-6-0,0:30:00,0:30:00,1-2,1 +U5-6-0,0:31:00,0:31:00,17-2,2 +U5-6-0,0:32:00,0:32:00,16-2,3 +U5-6-0,0:33:00,0:33:00,15-2,4 +U5-7-0,0:35:00,0:35:00,1-2,1 +U5-7-0,0:36:00,0:36:00,17-2,2 +U5-7-0,0:37:00,0:37:00,16-2,3 +U5-7-0,0:38:00,0:38:00,15-2,4 +U5-8-0,0:40:00,0:40:00,1-2,1 +U5-8-0,0:41:00,0:41:00,17-2,2 +U5-8-0,0:42:00,0:42:00,16-2,3 +U5-8-0,0:43:00,0:43:00,15-2,4 +U5-9-0,0:45:00,0:45:00,1-2,1 +U5-9-0,0:46:00,0:46:00,17-2,2 +U5-9-0,0:47:00,0:47:00,16-2,3 +U5-9-0,0:48:00,0:48:00,15-2,4 +U5-10-0,0:50:00,0:50:00,1-2,1 +U5-10-0,0:51:00,0:51:00,17-2,2 +U5-10-0,0:52:00,0:52:00,16-2,3 +U5-10-0,0:53:00,0:53:00,15-2,4 +U5-11-0,0:55:00,0:55:00,1-2,1 +U5-11-0,0:56:00,0:56:00,17-2,2 +U5-11-0,0:57:00,0:57:00,16-2,3 +U5-11-0,0:58:00,0:58:00,15-2,4 +U5-12-0,1:00:00,1:00:00,1-2,1 +U5-12-0,1:01:00,1:01:00,17-2,2 +U5-12-0,1:02:00,1:02:00,16-2,3 +U5-12-0,1:03:00,1:03:00,15-2,4 +U5-13-0,1:05:00,1:05:00,1-2,1 +U5-13-0,1:06:00,1:06:00,17-2,2 +U5-13-0,1:07:00,1:07:00,16-2,3 +U5-13-0,1:08:00,1:08:00,15-2,4 +U5-14-0,1:10:00,1:10:00,1-2,1 +U5-14-0,1:11:00,1:11:00,17-2,2 +U5-14-0,1:12:00,1:12:00,16-2,3 +U5-14-0,1:13:00,1:13:00,15-2,4 +U5-15-0,1:15:00,1:15:00,1-2,1 +U5-15-0,1:16:00,1:16:00,17-2,2 +U5-15-0,1:17:00,1:17:00,16-2,3 +U5-15-0,1:18:00,1:18:00,15-2,4 +U5-16-0,1:20:00,1:20:00,1-2,1 +U5-16-0,1:21:00,1:21:00,17-2,2 +U5-16-0,1:22:00,1:22:00,16-2,3 +U5-16-0,1:23:00,1:23:00,15-2,4 +U5-17-0,1:25:00,1:25:00,1-2,1 +U5-17-0,1:26:00,1:26:00,17-2,2 +U5-17-0,1:27:00,1:27:00,16-2,3 +U5-17-0,1:28:00,1:28:00,15-2,4 +U5-18-0,1:30:00,1:30:00,1-2,1 +U5-18-0,1:31:00,1:31:00,17-2,2 +U5-18-0,1:32:00,1:32:00,16-2,3 +U5-18-0,1:33:00,1:33:00,15-2,4 +U5-19-0,1:35:00,1:35:00,1-2,1 +U5-19-0,1:36:00,1:36:00,17-2,2 +U5-19-0,1:37:00,1:37:00,16-2,3 +U5-19-0,1:38:00,1:38:00,15-2,4 +U5-20-0,1:40:00,1:40:00,1-2,1 +U5-20-0,1:41:00,1:41:00,17-2,2 +U5-20-0,1:42:00,1:42:00,16-2,3 +U5-20-0,1:43:00,1:43:00,15-2,4 +U5-21-0,1:45:00,1:45:00,1-2,1 +U5-21-0,1:46:00,1:46:00,17-2,2 +U5-21-0,1:47:00,1:47:00,16-2,3 +U5-21-0,1:48:00,1:48:00,15-2,4 +U5-22-0,1:50:00,1:50:00,1-2,1 +U5-22-0,1:51:00,1:51:00,17-2,2 +U5-22-0,1:52:00,1:52:00,16-2,3 +U5-22-0,1:53:00,1:53:00,15-2,4 +U5-23-0,1:55:00,1:55:00,1-2,1 +U5-23-0,1:56:00,1:56:00,17-2,2 +U5-23-0,1:57:00,1:57:00,16-2,3 +U5-23-0,1:58:00,1:58:00,15-2,4 +U5-24-0,2:00:00,2:00:00,1-2,1 +U5-24-0,2:01:00,2:01:00,17-2,2 +U5-24-0,2:02:00,2:02:00,16-2,3 +U5-24-0,2:03:00,2:03:00,15-2,4 +U5-25-0,2:05:00,2:05:00,1-2,1 +U5-25-0,2:06:00,2:06:00,17-2,2 +U5-25-0,2:07:00,2:07:00,16-2,3 +U5-25-0,2:08:00,2:08:00,15-2,4 +U5-26-0,2:10:00,2:10:00,1-2,1 +U5-26-0,2:11:00,2:11:00,17-2,2 +U5-26-0,2:12:00,2:12:00,16-2,3 +U5-26-0,2:13:00,2:13:00,15-2,4 +U5-27-0,2:15:00,2:15:00,1-2,1 +U5-27-0,2:16:00,2:16:00,17-2,2 +U5-27-0,2:17:00,2:17:00,16-2,3 +U5-27-0,2:18:00,2:18:00,15-2,4 +U5-28-0,2:20:00,2:20:00,1-2,1 +U5-28-0,2:21:00,2:21:00,17-2,2 +U5-28-0,2:22:00,2:22:00,16-2,3 +U5-28-0,2:23:00,2:23:00,15-2,4 +U5-29-0,2:25:00,2:25:00,1-2,1 +U5-29-0,2:26:00,2:26:00,17-2,2 +U5-29-0,2:27:00,2:27:00,16-2,3 +U5-29-0,2:28:00,2:28:00,15-2,4 +U5-30-0,2:30:00,2:30:00,1-2,1 +U5-30-0,2:31:00,2:31:00,17-2,2 +U5-30-0,2:32:00,2:32:00,16-2,3 +U5-30-0,2:33:00,2:33:00,15-2,4 +U5-31-0,2:35:00,2:35:00,1-2,1 +U5-31-0,2:36:00,2:36:00,17-2,2 +U5-31-0,2:37:00,2:37:00,16-2,3 +U5-31-0,2:38:00,2:38:00,15-2,4 +U5-32-0,2:40:00,2:40:00,1-2,1 +U5-32-0,2:41:00,2:41:00,17-2,2 +U5-32-0,2:42:00,2:42:00,16-2,3 +U5-32-0,2:43:00,2:43:00,15-2,4 +U5-33-0,2:45:00,2:45:00,1-2,1 +U5-33-0,2:46:00,2:46:00,17-2,2 +U5-33-0,2:47:00,2:47:00,16-2,3 +U5-33-0,2:48:00,2:48:00,15-2,4 +U5-34-0,2:50:00,2:50:00,1-2,1 +U5-34-0,2:51:00,2:51:00,17-2,2 +U5-34-0,2:52:00,2:52:00,16-2,3 +U5-34-0,2:53:00,2:53:00,15-2,4 +U5-35-0,2:55:00,2:55:00,1-2,1 +U5-35-0,2:56:00,2:56:00,17-2,2 +U5-35-0,2:57:00,2:57:00,16-2,3 +U5-35-0,2:58:00,2:58:00,15-2,4 +U5-0-1,0:00:00,0:00:00,15-3,1 +U5-0-1,0:01:00,0:01:00,16-3,2 +U5-0-1,0:02:00,0:02:00,17-3,3 +U5-0-1,0:03:00,0:03:00,1-3,4 +U5-1-1,0:05:00,0:05:00,15-3,1 +U5-1-1,0:06:00,0:06:00,16-3,2 +U5-1-1,0:07:00,0:07:00,17-3,3 +U5-1-1,0:08:00,0:08:00,1-3,4 +U5-2-1,0:10:00,0:10:00,15-3,1 +U5-2-1,0:11:00,0:11:00,16-3,2 +U5-2-1,0:12:00,0:12:00,17-3,3 +U5-2-1,0:13:00,0:13:00,1-3,4 +U5-3-1,0:15:00,0:15:00,15-3,1 +U5-3-1,0:16:00,0:16:00,16-3,2 +U5-3-1,0:17:00,0:17:00,17-3,3 +U5-3-1,0:18:00,0:18:00,1-3,4 +U5-4-1,0:20:00,0:20:00,15-3,1 +U5-4-1,0:21:00,0:21:00,16-3,2 +U5-4-1,0:22:00,0:22:00,17-3,3 +U5-4-1,0:23:00,0:23:00,1-3,4 +U5-5-1,0:25:00,0:25:00,15-3,1 +U5-5-1,0:26:00,0:26:00,16-3,2 +U5-5-1,0:27:00,0:27:00,17-3,3 +U5-5-1,0:28:00,0:28:00,1-3,4 +U5-6-1,0:30:00,0:30:00,15-3,1 +U5-6-1,0:31:00,0:31:00,16-3,2 +U5-6-1,0:32:00,0:32:00,17-3,3 +U5-6-1,0:33:00,0:33:00,1-3,4 +U5-7-1,0:35:00,0:35:00,15-3,1 +U5-7-1,0:36:00,0:36:00,16-3,2 +U5-7-1,0:37:00,0:37:00,17-3,3 +U5-7-1,0:38:00,0:38:00,1-3,4 +U5-8-1,0:40:00,0:40:00,15-3,1 +U5-8-1,0:41:00,0:41:00,16-3,2 +U5-8-1,0:42:00,0:42:00,17-3,3 +U5-8-1,0:43:00,0:43:00,1-3,4 +U5-9-1,0:45:00,0:45:00,15-3,1 +U5-9-1,0:46:00,0:46:00,16-3,2 +U5-9-1,0:47:00,0:47:00,17-3,3 +U5-9-1,0:48:00,0:48:00,1-3,4 +U5-10-1,0:50:00,0:50:00,15-3,1 +U5-10-1,0:51:00,0:51:00,16-3,2 +U5-10-1,0:52:00,0:52:00,17-3,3 +U5-10-1,0:53:00,0:53:00,1-3,4 +U5-11-1,0:55:00,0:55:00,15-3,1 +U5-11-1,0:56:00,0:56:00,16-3,2 +U5-11-1,0:57:00,0:57:00,17-3,3 +U5-11-1,0:58:00,0:58:00,1-3,4 +U5-12-1,1:00:00,1:00:00,15-3,1 +U5-12-1,1:01:00,1:01:00,16-3,2 +U5-12-1,1:02:00,1:02:00,17-3,3 +U5-12-1,1:03:00,1:03:00,1-3,4 +U5-13-1,1:05:00,1:05:00,15-3,1 +U5-13-1,1:06:00,1:06:00,16-3,2 +U5-13-1,1:07:00,1:07:00,17-3,3 +U5-13-1,1:08:00,1:08:00,1-3,4 +U5-14-1,1:10:00,1:10:00,15-3,1 +U5-14-1,1:11:00,1:11:00,16-3,2 +U5-14-1,1:12:00,1:12:00,17-3,3 +U5-14-1,1:13:00,1:13:00,1-3,4 +U5-15-1,1:15:00,1:15:00,15-3,1 +U5-15-1,1:16:00,1:16:00,16-3,2 +U5-15-1,1:17:00,1:17:00,17-3,3 +U5-15-1,1:18:00,1:18:00,1-3,4 +U5-16-1,1:20:00,1:20:00,15-3,1 +U5-16-1,1:21:00,1:21:00,16-3,2 +U5-16-1,1:22:00,1:22:00,17-3,3 +U5-16-1,1:23:00,1:23:00,1-3,4 +U5-17-1,1:25:00,1:25:00,15-3,1 +U5-17-1,1:26:00,1:26:00,16-3,2 +U5-17-1,1:27:00,1:27:00,17-3,3 +U5-17-1,1:28:00,1:28:00,1-3,4 +U5-18-1,1:30:00,1:30:00,15-3,1 +U5-18-1,1:31:00,1:31:00,16-3,2 +U5-18-1,1:32:00,1:32:00,17-3,3 +U5-18-1,1:33:00,1:33:00,1-3,4 +U5-19-1,1:35:00,1:35:00,15-3,1 +U5-19-1,1:36:00,1:36:00,16-3,2 +U5-19-1,1:37:00,1:37:00,17-3,3 +U5-19-1,1:38:00,1:38:00,1-3,4 +U5-20-1,1:40:00,1:40:00,15-3,1 +U5-20-1,1:41:00,1:41:00,16-3,2 +U5-20-1,1:42:00,1:42:00,17-3,3 +U5-20-1,1:43:00,1:43:00,1-3,4 +U5-21-1,1:45:00,1:45:00,15-3,1 +U5-21-1,1:46:00,1:46:00,16-3,2 +U5-21-1,1:47:00,1:47:00,17-3,3 +U5-21-1,1:48:00,1:48:00,1-3,4 +U5-22-1,1:50:00,1:50:00,15-3,1 +U5-22-1,1:51:00,1:51:00,16-3,2 +U5-22-1,1:52:00,1:52:00,17-3,3 +U5-22-1,1:53:00,1:53:00,1-3,4 +U5-23-1,1:55:00,1:55:00,15-3,1 +U5-23-1,1:56:00,1:56:00,16-3,2 +U5-23-1,1:57:00,1:57:00,17-3,3 +U5-23-1,1:58:00,1:58:00,1-3,4 +U5-24-1,2:00:00,2:00:00,15-3,1 +U5-24-1,2:01:00,2:01:00,16-3,2 +U5-24-1,2:02:00,2:02:00,17-3,3 +U5-24-1,2:03:00,2:03:00,1-3,4 +U5-25-1,2:05:00,2:05:00,15-3,1 +U5-25-1,2:06:00,2:06:00,16-3,2 +U5-25-1,2:07:00,2:07:00,17-3,3 +U5-25-1,2:08:00,2:08:00,1-3,4 +U5-26-1,2:10:00,2:10:00,15-3,1 +U5-26-1,2:11:00,2:11:00,16-3,2 +U5-26-1,2:12:00,2:12:00,17-3,3 +U5-26-1,2:13:00,2:13:00,1-3,4 +U5-27-1,2:15:00,2:15:00,15-3,1 +U5-27-1,2:16:00,2:16:00,16-3,2 +U5-27-1,2:17:00,2:17:00,17-3,3 +U5-27-1,2:18:00,2:18:00,1-3,4 +U5-28-1,2:20:00,2:20:00,15-3,1 +U5-28-1,2:21:00,2:21:00,16-3,2 +U5-28-1,2:22:00,2:22:00,17-3,3 +U5-28-1,2:23:00,2:23:00,1-3,4 +U5-29-1,2:25:00,2:25:00,15-3,1 +U5-29-1,2:26:00,2:26:00,16-3,2 +U5-29-1,2:27:00,2:27:00,17-3,3 +U5-29-1,2:28:00,2:28:00,1-3,4 +U5-30-1,2:30:00,2:30:00,15-3,1 +U5-30-1,2:31:00,2:31:00,16-3,2 +U5-30-1,2:32:00,2:32:00,17-3,3 +U5-30-1,2:33:00,2:33:00,1-3,4 +U5-31-1,2:35:00,2:35:00,15-3,1 +U5-31-1,2:36:00,2:36:00,16-3,2 +U5-31-1,2:37:00,2:37:00,17-3,3 +U5-31-1,2:38:00,2:38:00,1-3,4 +U5-32-1,2:40:00,2:40:00,15-3,1 +U5-32-1,2:41:00,2:41:00,16-3,2 +U5-32-1,2:42:00,2:42:00,17-3,3 +U5-32-1,2:43:00,2:43:00,1-3,4 +U5-33-1,2:45:00,2:45:00,15-3,1 +U5-33-1,2:46:00,2:46:00,16-3,2 +U5-33-1,2:47:00,2:47:00,17-3,3 +U5-33-1,2:48:00,2:48:00,1-3,4 +U5-34-1,2:50:00,2:50:00,15-3,1 +U5-34-1,2:51:00,2:51:00,16-3,2 +U5-34-1,2:52:00,2:52:00,17-3,3 +U5-34-1,2:53:00,2:53:00,1-3,4 +U5-35-1,2:55:00,2:55:00,15-3,1 +U5-35-1,2:56:00,2:56:00,16-3,2 +U5-35-1,2:57:00,2:57:00,17-3,3 +U5-35-1,2:58:00,2:58:00,1-3,4 diff --git a/data/pt/example_network/example_gtfs/matched/stops_fp.txt b/data/pt/example_network/example_gtfs/matched/stops_fp.txt new file mode 100644 index 00000000..0c9db8bd --- /dev/null +++ b/data/pt/example_network/example_gtfs/matched/stops_fp.txt @@ -0,0 +1,39 @@ +stop_id +1-0 +1-1 +1-2 +1-3 +2-0 +2-1 +3-0 +3-1 +4-0 +4-1 +5-0 +5-1 +6-0 +6-1 +7-0 +7-1 +8-0 +8-1 +9-0 +9-1 +10-0 +10-1 +11-0 +11-1 +12-0 +12-1 +13-0 +13-1 +14-0 +14-1 +15-0 +15-1 +15-2 +15-3 +16-2 +16-3 +17-2 +17-3 diff --git a/data/pt/example_network/example_gtfs/matched/street_station_transfers_fp.txt b/data/pt/example_network/example_gtfs/matched/street_station_transfers_fp.txt new file mode 100644 index 00000000..bdc34d27 --- /dev/null +++ b/data/pt/example_network/example_gtfs/matched/street_station_transfers_fp.txt @@ -0,0 +1,29 @@ +node_id,closest_station_id,street_station_transfer_time +2966,s1,60 +2967,s2,27 +2968,s3,49 +2969,s4,27 +2970,s5,12 +2971,s6,66 +2972,s7,3 +2973,s8,19 +2974,s9,30 +2975,s10,46 +2976,s11,23 +2977,s12,17 +2978,s13,29 +2979,s14,38 +2980,s15,28 +2981,s14,23 +2982,s13,21 +2983,s12,18 +2984,s11,40 +2985,s10,40 +2986,s9,35 +2987,s8,12 +2988,s7,4 +2989,s6,64 +2990,s5,13 +2991,s4,38 +2992,s3,58 +2993,s2,27 diff --git a/data/pt/example_network/example_gtfs/matched/transfers_fp.txt b/data/pt/example_network/example_gtfs/matched/transfers_fp.txt new file mode 100644 index 00000000..51a2fa24 --- /dev/null +++ b/data/pt/example_network/example_gtfs/matched/transfers_fp.txt @@ -0,0 +1,55 @@ +from_stop_id,to_stop_id,min_transfer_time +1-0,1-1,3 +1-0,1-2,3 +1-0,1-3,3 +1-1,1-0,3 +1-1,1-2,3 +1-1,1-3,3 +1-2,1-0,3 +1-2,1-1,3 +1-2,1-3,3 +1-3,1-0,3 +1-3,1-1,3 +1-3,1-2,3 +2-0,2-1,3 +2-1,2-0,3 +3-0,3-1,3 +3-1,3-0,3 +4-0,4-1,3 +4-1,4-0,3 +5-0,5-1,3 +5-1,5-0,3 +6-0,6-1,3 +6-1,6-0,3 +7-0,7-1,3 +7-1,7-0,3 +8-0,8-1,3 +8-1,8-0,3 +9-0,9-1,3 +9-1,9-0,3 +10-0,10-1,3 +10-1,10-0,3 +11-0,11-1,3 +11-1,11-0,3 +12-0,12-1,3 +12-1,12-0,3 +13-0,13-1,3 +13-1,13-0,3 +14-0,14-1,3 +14-1,14-0,3 +15-0,15-1,3 +15-0,15-2,3 +15-0,15-3,3 +15-1,15-0,3 +15-1,15-2,3 +15-1,15-3,3 +15-2,15-0,3 +15-2,15-1,3 +15-2,15-3,3 +15-3,15-0,3 +15-3,15-1,3 +15-3,15-2,3 +16-2,16-3,3 +16-3,16-2,3 +17-2,17-3,3 +17-3,17-2,3 diff --git a/data/pt/example_network/example_gtfs/matched/trips_fp.txt b/data/pt/example_network/example_gtfs/matched/trips_fp.txt new file mode 100644 index 00000000..a23bc74b --- /dev/null +++ b/data/pt/example_network/example_gtfs/matched/trips_fp.txt @@ -0,0 +1,94 @@ +route_id,trip_id,service_id,direction_id +196,196-0-0,0,0 +196,196-1-0,0,0 +196,196-2-0,0,0 +196,196-3-0,0,0 +196,196-4-0,0,0 +196,196-5-0,0,0 +196,196-6-0,0,0 +196,196-7-0,0,0 +196,196-8-0,0,0 +196,196-0-1,0,1 +196,196-1-1,0,1 +196,196-2-1,0,1 +196,196-3-1,0,1 +196,196-4-1,0,1 +196,196-5-1,0,1 +196,196-6-1,0,1 +196,196-7-1,0,1 +196,196-8-1,0,1 +U5,U5-0-0,0,0 +U5,U5-1-0,0,0 +U5,U5-2-0,0,0 +U5,U5-3-0,0,0 +U5,U5-4-0,0,0 +U5,U5-5-0,0,0 +U5,U5-6-0,0,0 +U5,U5-7-0,0,0 +U5,U5-8-0,0,0 +U5,U5-9-0,0,0 +U5,U5-10-0,0,0 +U5,U5-11-0,0,0 +U5,U5-12-0,0,0 +U5,U5-13-0,0,0 +U5,U5-14-0,0,0 +U5,U5-15-0,0,0 +U5,U5-16-0,0,0 +U5,U5-17-0,0,0 +U5,U5-18-0,0,0 +U5,U5-19-0,0,0 +U5,U5-20-0,0,0 +U5,U5-21-0,0,0 +U5,U5-22-0,0,0 +U5,U5-23-0,0,0 +U5,U5-24-0,0,0 +U5,U5-25-0,0,0 +U5,U5-26-0,0,0 +U5,U5-27-0,0,0 +U5,U5-28-0,0,0 +U5,U5-29-0,0,0 +U5,U5-30-0,0,0 +U5,U5-31-0,0,0 +U5,U5-32-0,0,0 +U5,U5-33-0,0,0 +U5,U5-34-0,0,0 +U5,U5-35-0,0,0 +U5,U5-0-1,0,1 +U5,U5-1-1,0,1 +U5,U5-2-1,0,1 +U5,U5-3-1,0,1 +U5,U5-4-1,0,1 +U5,U5-5-1,0,1 +U5,U5-6-1,0,1 +U5,U5-7-1,0,1 +U5,U5-8-1,0,1 +U5,U5-9-1,0,1 +U5,U5-10-1,0,1 +U5,U5-11-1,0,1 +U5,U5-12-1,0,1 +U5,U5-13-1,0,1 +U5,U5-14-1,0,1 +U5,U5-15-1,0,1 +U5,U5-16-1,0,1 +U5,U5-17-1,0,1 +U5,U5-18-1,0,1 +U5,U5-19-1,0,1 +U5,U5-20-1,0,1 +U5,U5-21-1,0,1 +U5,U5-22-1,0,1 +U5,U5-23-1,0,1 +U5,U5-24-1,0,1 +U5,U5-25-1,0,1 +U5,U5-26-1,0,1 +U5,U5-27-1,0,1 +U5,U5-28-1,0,1 +U5,U5-29-1,0,1 +U5,U5-30-1,0,1 +U5,U5-31-1,0,1 +U5,U5-32-1,0,1 +U5,U5-33-1,0,1 +U5,U5-34-1,0,1 +U5,U5-35-1,0,1 + + + diff --git a/data/pubtrans/route_193/193_line_alignment.geojson b/data/pt/route_193/193_line_alignment.geojson similarity index 100% rename from data/pubtrans/route_193/193_line_alignment.geojson rename to data/pt/route_193/193_line_alignment.geojson diff --git a/data/pubtrans/route_193/193_schedules.csv b/data/pt/route_193/193_schedules.csv similarity index 100% rename from data/pubtrans/route_193/193_schedules.csv rename to data/pt/route_193/193_schedules.csv diff --git a/data/pubtrans/route_193/stations.csv b/data/pt/route_193/stations.csv similarity index 100% rename from data/pubtrans/route_193/stations.csv rename to data/pt/route_193/stations.csv diff --git a/run_examples.py b/run_examples.py index 5f9f6541..58748628 100644 --- a/run_examples.py +++ b/run_examples.py @@ -317,9 +317,15 @@ def check_assertions(list_eval_df, all_scenario_assertion_dict): sc = os.path.join(scs_path, "example_rpp.csv") run_scenarios(cc, sc, log_level=log_level, n_cpu_per_sim=1, n_parallel_sim=1) + # n) Pooling with Intermodal demand + log_level = "info" + cc = os.path.join(scs_path, "constant_config_ir.csv") + sc = os.path.join(scs_path, "example_im.csv") + run_scenarios(cc, sc, log_level=log_level, n_cpu_per_sim=1, n_parallel_sim=1) + # i) Semi-on-Demand Public Transit example # Run PTScheduleGen.py to generate required PT files first log_level = "info" cc = os.path.join(scs_path, "constant_config_sod.csv") sc = os.path.join(scs_path, "example_sod.csv") - run_scenarios(cc, sc, log_level=log_level, n_cpu_per_sim=1, n_parallel_sim=1) + run_scenarios(cc, sc, log_level=log_level, n_cpu_per_sim=1, n_parallel_sim=1) \ No newline at end of file diff --git a/src/FleetSimulationBase.py b/src/FleetSimulationBase.py index acf621c5..d4f5b62b 100644 --- a/src/FleetSimulationBase.py +++ b/src/FleetSimulationBase.py @@ -21,14 +21,15 @@ # src imports # ----------- -from src.misc.init_modules import load_fleet_control_module, load_routing_engine, load_broker_module +from src.misc.init_modules import load_fleet_control_module, load_routing_engine, load_broker_module, load_pt_control_module from src.demand.demand import Demand, SlaveDemand from src.simulation.Vehicles import SimulationVehicle if tp.TYPE_CHECKING: from src.fleetctrl.FleetControlBase import FleetControlBase - from src.routing.NetworkBase import NetworkBase + from src.routing.road.NetworkBase import NetworkBase from src.broker.BrokerBase import BrokerBase from src.python_plots.plot_classes import PyPlot + from src.ptctrl.PTControlBase import PTControlBase # -------------------------------------------------------------------------------------------------------------------- # # global variables @@ -251,21 +252,23 @@ def __init__(self, scenario_parameters: dict): self.network_stat_f) # public transportation module LOG.info("Initialization of line-based public transportation...") - pt_type = self.scenario_parameters.get(G_PT_TYPE) - self.gtfs_data_dir = self.dir_names.get(G_DIR_PT) - if pt_type is None or self.gtfs_data_dir is None: - self.pt = None - elif pt_type == "PTMatrixCrowding": - pt_module = importlib.import_module("src.pubtrans.PtTTMatrixCrowding") - self.pt = pt_module.PublicTransportTravelTimeMatrixWithCrowding(self.gtfs_data_dir, self.pt_stat_f, - self.scenario_parameters, - self.routing_engine, self.zones) - elif pt_type == "PtCrowding": - pt_module = importlib.import_module("src.pubtrans.PtCrowding") - self.pt = pt_module.PublicTransportWithCrowding(self.gtfs_data_dir, self.pt_stat_f, self.scenario_parameters, - self.routing_engine, self.zones) - else: - raise IOError(f"Public transport module {pt_type} not defined for current simulation environment.") + # pt_type = self.scenario_parameters.get(G_PT_TYPE) + # self.gtfs_data_dir = self.dir_names.get(G_DIR_PT) + # if pt_type is None or self.gtfs_data_dir is None: + # self.pt = None + # elif pt_type == "PTMatrixCrowding": + # pt_module = importlib.import_module("src.pt.PtTTMatrixCrowding") + # self.pt = pt_module.PublicTransportTravelTimeMatrixWithCrowding(self.gtfs_data_dir, self.pt_stat_f, + # self.scenario_parameters, + # self.routing_engine, self.zones) + # elif pt_type == "PtCrowding": + # pt_module = importlib.import_module("src.pt.PtCrowding") + # self.pt = pt_module.PublicTransportWithCrowding(self.gtfs_data_dir, self.pt_stat_f, self.scenario_parameters, + # self.routing_engine, self.zones) + # else: + # raise IOError(f"Public transport module {pt_type} not defined for current simulation environment.") + self.pt_operator: PTControlBase = None + self._load_pt_operator() # attribute for demand, charging and zone module self.demand = None @@ -456,17 +459,37 @@ def _load_fleetctr_vehicles(self): def _load_broker_module(self): """ Loads the broker """ - - if self.scenario_parameters.get(G_BROKER_TYPE) is None: - LOG.info("No broker type specified, using default broker: BrokerBasic.") - op_broker_class_string = "BrokerBasic" - BrokerClass = load_broker_module(op_broker_class_string) + + broker_type: str = self.scenario_parameters.get(G_BROKER_TYPE, None) + implemented_brokers = ["PTBroker", "PTBrokerEI", "PTBrokerPAYG"] + if broker_type is None: + prt_msg: str = "No broker type specified, using default BrokerBasic" + LOG.info(prt_msg) + BrokerClass = load_broker_module("BrokerBasic") self.broker = BrokerClass(self.n_op, self.operators) + elif broker_type in implemented_brokers: + prt_msg: str = f"Broker specified, using {broker_type}" + LOG.info(prt_msg) + if self.pt_operator is None: + raise ValueError("PT operator should be loaded before loading PTBroker.") + BrokerClass = load_broker_module(broker_type) + self.broker = BrokerClass(self.n_op, self.operators, self.pt_operator, self.demand, self.routing_engine, self.scenario_parameters) else: - LOG.info(f"Broker type specified: {self.scenario_parameters.get(G_BROKER_TYPE)}") - op_broker_class_string = self.scenario_parameters.get(G_BROKER_TYPE) - BrokerClass = load_broker_module(op_broker_class_string) - self.broker = BrokerClass(self.n_op, self.operators) + raise ValueError(f"Unknown broker type: {broker_type}!") + print('Broker: ' + prt_msg + '\n') + + def _load_pt_operator(self): + """ Loads the public transport operator """ + + pt_operator_type = self.scenario_parameters.get(G_PT_OPERATOR_TYPE, None) + gtfs_data_dir = self.dir_names.get(G_DIR_GTFS) + pt_operator_id = self.scenario_parameters.get(G_PT_OPERATOR_ID, -2) + if pt_operator_type is None or gtfs_data_dir is None: + return + else: + LOG.info(f"Public transport operator type specified: {pt_operator_type}") + PTControlClass = load_pt_control_module(pt_operator_type) + self.pt_operator = PTControlClass(gtfs_data_dir, pt_operator_id) @staticmethod def get_directory_dict(scenario_parameters, list_operator_dicts): @@ -491,9 +514,16 @@ def save_scenario_inputs(self): def evaluate(self): """Runs standard and simulation environment specific evaluations over simulation results.""" output_dir = self.dir_names[G_DIR_OUTPUT] - # standard evaluation - from src.evaluation.standard import standard_evaluation - standard_evaluation(output_dir) + evaluation_method = self.scenario_parameters.get(G_EVAL_METHOD, 'standard_evaluation') + if evaluation_method == 'standard_evaluation': + # standard evaluation + from src.evaluation.standard import standard_evaluation + standard_evaluation(output_dir) + elif evaluation_method == 'intermodal_evaluation': + from src.evaluation.intermodal import intermodal_evaluation + intermodal_evaluation(output_dir) + else: + raise ValueError(f"Unknown evaluation method {evaluation_method} specified!") self.add_evaluate() # def initialize_operators_and_vehicles(self): TODO I think this is depricated! @@ -684,20 +714,20 @@ def update_sim_state_fleets(self, last_time, next_time, force_update_plan=False) self.vehicle_update_order[opid_vid_tuple] = 0 else: self.vehicle_update_order[opid_vid_tuple] = 1 - for rid, boarding_time_and_pos in boarding_requests.items(): + for rid_struct, boarding_time_and_pos in boarding_requests.items(): # rid_struct is the actual key boarding_time, boarding_pos = boarding_time_and_pos - LOG.debug(f"rid {rid} boarding at {boarding_time} at pos {boarding_pos}") - self.demand.record_boarding(rid, vid, op_id, boarding_time, pu_pos=boarding_pos) - self.broker.acknowledge_user_boarding(op_id, rid, vid, boarding_time) - for rid, alighting_start_time_and_pos in dict_start_alighting.items(): + LOG.debug(f"rid {rid_struct} boarding at {boarding_time} at pos {boarding_pos}") + self.demand.record_boarding(rid_struct, vid, op_id, boarding_time, pu_pos=boarding_pos) + self.broker.acknowledge_user_boarding(op_id, rid_struct, vid, boarding_time) + for rid_struct, alighting_start_time_and_pos in dict_start_alighting.items(): # record user stats at beginning of alighting process alighting_start_time, alighting_pos = alighting_start_time_and_pos - LOG.debug(f"rid {rid} deboarding at {alighting_start_time} at pos {alighting_pos}") - self.demand.record_alighting_start(rid, vid, op_id, alighting_start_time, do_pos=alighting_pos) - for rid, alighting_end_time in alighting_requests.items(): + LOG.debug(f"rid {rid_struct} deboarding at {alighting_start_time} at pos {alighting_pos}") + self.demand.record_alighting_start(rid_struct, vid, op_id, alighting_start_time, do_pos=alighting_pos) + for rid_struct, alighting_end_time in alighting_requests.items(): # # record user stats at end of alighting process - self.demand.user_ends_alighting(rid, vid, op_id, alighting_end_time) - self.broker.acknowledge_user_alighting(op_id, rid, vid, alighting_end_time) + self.demand.user_ends_alighting(rid_struct, vid, op_id, alighting_end_time) + self.broker.acknowledge_user_alighting(op_id, rid_struct, vid, alighting_end_time) # send update to operator if len(boarding_requests) > 0 or len(dict_start_alighting) > 0: self.broker.receive_status_update(op_id, vid, next_time, passed_VRL, True) diff --git a/src/ImmediateDecisionsSimulation.py b/src/ImmediateDecisionsSimulation.py index 62ab061b..863e8531 100644 --- a/src/ImmediateDecisionsSimulation.py +++ b/src/ImmediateDecisionsSimulation.py @@ -88,7 +88,7 @@ def step(self, sim_time): # 3) for rid, rq_obj in list_undecided_travelers + list_new_traveler_rid_obj: self.broker.inform_request(rid, rq_obj, sim_time) - amod_offers = self.broker.collect_offers(rid) + amod_offers = self.broker.collect_offers(rid, sim_time) for op_id, amod_offer in amod_offers.items(): rq_obj.receive_offer(op_id, amod_offer, sim_time) self._rid_chooses_offer(rid, rq_obj, sim_time) diff --git a/src/broker/BrokerBase.py b/src/broker/BrokerBase.py index e04f74ca..266567ef 100644 --- a/src/broker/BrokerBase.py +++ b/src/broker/BrokerBase.py @@ -65,7 +65,7 @@ def inform_request(self, rid: int, rq_obj: 'RequestBase', sim_time: int): pass @abstractmethod - def collect_offers(self, rid: int) -> tp.Dict[int, 'RequestBase']: + def collect_offers(self, rid: int, sim_time: int = None) -> tp.Dict[int, 'RequestBase']: """This method collects the offers from the operators. The return value is a list of tuples, where each tuple contains the operator id, the offer, and the simulation time. """ diff --git a/src/broker/BrokerBasic.py b/src/broker/BrokerBasic.py index 876d5491..79abd183 100644 --- a/src/broker/BrokerBasic.py +++ b/src/broker/BrokerBasic.py @@ -43,6 +43,7 @@ def __init__(self, n_amod_op: int, amod_operators: tp.List['FleetControlBase']): The general attributes for the broker are initialized. Args: + n_amod_op (int): number of AMoD operators amod_operators (tp.List['FleetControlBase']): list of AMoD operators """ super().__init__(n_amod_op, amod_operators) @@ -62,7 +63,7 @@ def inform_request(self, rid: int, rq_obj: 'RequestBase', sim_time: int): LOG.debug(f"Request {rid}: To operator {op_id} ...") self.amod_operators[op_id].user_request(rq_obj, sim_time) - def collect_offers(self, rid: int) -> tp.Dict[int, 'RequestBase']: + def collect_offers(self, rid: int, sim_time: int = None) -> tp.Dict[int, 'RequestBase']: """This method collects the offers from the amod operators. The return value is a list of tuples, where each tuple contains the operator id, the offer, and the simulation time. """ diff --git a/src/broker/PTBroker.py b/src/broker/PTBroker.py new file mode 100644 index 00000000..fc4244d7 --- /dev/null +++ b/src/broker/PTBroker.py @@ -0,0 +1,360 @@ +# TODO: +# - Adjust PT waiting time based on dynamic GTFS data (e.g., delays), and then adjust FM and LM offers accordingly. +# - Support multiple AMoD operators for firstlastmile requests. + +# -------------------------------------------------------------------------------------------------------------------- # +# PTBroker: Collaborative Coordination Strategy +# +# Simulates a future scenario with autonomous DRT and tight MaaS–DRT integration: +# - MaaS queries DRT for FM and immediately receives a predicted dropoff time (actual offer, not estimated). +# - MaaS books PT and LM DRT based on that predicted dropoff time. +# - PT returns the user's expected waiting time at the boarding station; MaaS feeds this back to DRT. +# - DRT dynamically adjusts the user's latest dropoff deadline, giving DRT more flexibility for +# ride-pooling while still ensuring the user catches the PT vehicle. +# - MaaS can also constrain LM DRT waiting time, minimizing wait at the destination station. +# Result: higher service rate and shorter travel times through real-time coordination between MaaS and DRT. +# +# NOTE: This code has only been tested and applied in the ImmediateDecisionsSimulation environment +# combined with the PoolingIRSOnly fleet controller. +# -------------------------------------------------------------------------------------------------------------------- # + +# -------------------------------------------------------------------------------------------------------------------- # +# standard distribution imports +# ----------------------------- +import logging +from datetime import datetime, timedelta +import typing as tp +import pandas as pd +# additional module imports (> requirements) +# ------------------------------------------ + + +# src imports +# ----------- +from src.broker.PTBrokerBasic import PTBrokerBasic +from src.simulation.Offers import IntermodalOffer +if tp.TYPE_CHECKING: + from src.fleetctrl.FleetControlBase import FleetControlBase + from src.fleetctrl.planning.PlanRequest import PlanRequest + from src.ptctrl.PTControlBase import PTControlBase + from src.demand.demand import Demand + from src.routing.road.NetworkBase import NetworkBase + from src.demand.TravelerModels import RequestBase, BasicIntermodalRequest + from src.simulation.Offers import TravellerOffer, PTOffer + +# -------------------------------------------------------------------------------------------------------------------- # +# global variables +# ---------------- +from src.misc.globals import * + +LOG = logging.getLogger(__name__) +LARGE_INT = 100000000 +BUFFER_SIZE = 100 + +INPUT_PARAMETERS_PTBroker = { + "doc" : "this class represents a broker platform which handles intermodal requests", + "inherit" : PTBrokerBasic, + "input_parameters_mandatory": ["n_amod_op", "amod_operators", "pt_operator", "demand", "routing_engine", "scenario_parameters"], + "input_parameters_optional": [], + "mandatory_modules": [], + "optional_modules": [] +} + +# -------------------------------------------------------------------------------------------------------------------- # +# main +# ---- +class PTBroker(PTBrokerBasic): + def __init__( + self, + n_amod_op: int, + amod_operators: tp.List['FleetControlBase'], + pt_operator: 'PTControlBase', + demand: 'Demand', + routing_engine: 'NetworkBase', + scenario_parameters: dict, + ): + """ + The general attributes for the broker are initialized. + + Args: + n_amod_op (int): number of AMoD operators + amod_operators (tp.List['FleetControlBase']): list of AMoD operators + pt_operator (PTControlBase): PT operator + demand (Demand): demand object + routing_engine (NetworkBase): routing engine + scenario_parameters (dict): scenario parameters + """ + super().__init__(n_amod_op, amod_operators, pt_operator, demand, routing_engine, scenario_parameters) + + def _inform_amod_sub_request( + self, rq_obj: 'RequestBase', sub_trip_id: int, leg_o_node: int, leg_d_node: int, leg_start_time: int, + parent_modal_state: RQ_MODAL_STATE, op_id: int, sim_time: int + ): + """Overrides PTBrokerBasic to support customizable max_wait_time for last-mile AMoD pickups.""" + amod_sub_rq_obj: 'RequestBase' = self.demand.create_sub_requests(rq_obj, sub_trip_id, leg_o_node, leg_d_node, leg_start_time, parent_modal_state) + LOG.debug(f"AMoD sub-request {amod_sub_rq_obj.get_rid_struct()} with modal state {parent_modal_state}: To operator {op_id} ...") + + # get customizable wait time for last mile AMoD pickups + if parent_modal_state == RQ_MODAL_STATE.LASTMILE or (parent_modal_state == RQ_MODAL_STATE.FIRSTLASTMILE and sub_trip_id == RQ_SUB_TRIP_ID.FLM_AMOD_1.value): + max_wait_time: tp.Optional[int] = rq_obj.get_lastmile_max_wait_time() + else: + max_wait_time: tp.Optional[int] = None + + self.amod_operators[op_id].user_request(amod_sub_rq_obj, sim_time, max_wait_time=max_wait_time) + + def _process_inform_firstmile_request(self, rid: int, rq_obj: 'BasicIntermodalRequest', sim_time: int, parent_modal_state: RQ_MODAL_STATE = RQ_MODAL_STATE.FIRSTMILE): + """This method processes the new firstmile request. + In this stage, only the first-mile AMoD sub-request is created first; the PT sub-request will be created after receiving the AMoD offer. + + Args: + rid (int): the request id + rq_obj ('BasicIntermodalRequest'): the request object + sim_time (int): the simulation time + parent_modal_state (RQ_MODAL_STATE): the parent modal state + """ + # get the transfer station id and its closest pt station + transfer_station_ids: tp.List[str] = rq_obj.get_transfer_station_ids() + transfer_street_node, _ = self._find_transfer_info(transfer_station_ids[0], "pt2street") + + # create sub-request for AMoD + for op_id in range(self.n_amod_op): + self._inform_amod_sub_request(rq_obj, RQ_SUB_TRIP_ID.FM_AMOD.value, rq_obj.get_origin_node(), transfer_street_node, rq_obj.earliest_start_time, parent_modal_state, op_id, sim_time) + + def _process_inform_lastmile_request(self, rid: int, rq_obj: 'BasicIntermodalRequest', sim_time: int, parent_modal_state: RQ_MODAL_STATE = RQ_MODAL_STATE.LASTMILE): + """This method processes the new lastmile request. + First, the PT sub-request is created. If the PT offer is available, then the last-mile AMoD sub-request is created. + + Args: + rid (int): the request id + rq_obj ('BasicIntermodalRequest'): the request object + sim_time (int): the simulation time + parent_modal_state (RQ_MODAL_STATE): the parent modal state + """ + # get the transfer station id and its closest pt station + transfer_station_ids: tp.List[str] = rq_obj.get_transfer_station_ids() + transfer_street_node, _ = self._find_transfer_info(transfer_station_ids[0], "pt2street") + # create sub-request for PT + lm_pt_arrival: tp.Optional[int] = self._inform_pt_sub_request(rq_obj, RQ_SUB_TRIP_ID.LM_PT.value, rq_obj.get_origin_node(), transfer_street_node, rq_obj.earliest_start_time, parent_modal_state) + + if lm_pt_arrival is not None: + # create sub-request for AMoD + for op_id in range(self.n_amod_op): + self._inform_amod_sub_request(rq_obj, RQ_SUB_TRIP_ID.LM_AMOD.value, transfer_street_node, rq_obj.get_destination_node(), lm_pt_arrival, parent_modal_state, op_id, sim_time) + else: + LOG.info(f"PT offer is not available for sub_request {rid}_{RQ_SUB_TRIP_ID.LM_PT.value}, so the lastmile AMoD sub-request will not be created.") + + def _process_inform_firstlastmile_request(self, rid: int, rq_obj: 'BasicIntermodalRequest', sim_time: int, parent_modal_state: RQ_MODAL_STATE = RQ_MODAL_STATE.FIRSTLASTMILE): + """This method processes the new firstlastmile request. + In this stage, only the first-mile AMoD sub-request is created first; the PT and last-mile AMoD sub-requests will be created after receiving the first-mile AMoD offer. + + Args: + rid (int): the request id + rq_obj ('BasicIntermodalRequest'): the request object + sim_time (int): the simulation time + parent_modal_state (RQ_MODAL_STATE): the parent modal state + """ + # get the transfer station ids and their closest pt stations + transfer_station_ids: tp.List[str] = rq_obj.get_transfer_station_ids() + transfer_street_node_0, _ = self._find_transfer_info(transfer_station_ids[0], "pt2street") + + # create FM sub-request for AMoD + for op_id in range(self.n_amod_op): + # firstmile AMoD sub-request + self._inform_amod_sub_request(rq_obj, RQ_SUB_TRIP_ID.FLM_AMOD_0.value, rq_obj.get_origin_node(), transfer_street_node_0, rq_obj.earliest_start_time, parent_modal_state, op_id, sim_time) + + def _process_collect_firstmile_offers( + self, rid: int, parent_rq_obj: 'BasicIntermodalRequest', parent_modal_state: RQ_MODAL_STATE, + offers: tp.Dict[int, 'TravellerOffer'] + ) -> tp.Dict[int, 'TravellerOffer']: + """This method processes the collection of firstmile offers and try to optimize the waiting time of the PT leg. + """ + # get rid struct for all sections + fm_amod_rid_struct: str = f"{rid}_{RQ_SUB_TRIP_ID.FM_AMOD.value}" + fm_pt_rid_struct: str = f"{rid}_{RQ_SUB_TRIP_ID.FM_PT.value}" + + for amod_op_id in range(self.n_amod_op): + # collect FM offers + fm_amod_offer: 'TravellerOffer' = self.amod_operators[amod_op_id].get_current_offer(fm_amod_rid_struct) + LOG.debug(f"Collecting fm_amod offer for request {fm_amod_rid_struct} from operator {amod_op_id}: {fm_amod_offer}.") + + # check if FM AMoD offer is available + if fm_amod_offer is None or fm_amod_offer.service_declined(): + LOG.info(f"FM AMoD offer is not available for sub_request {fm_amod_rid_struct}, skipping to next AMoD operator.") + continue + # register the FM AMoD offer in the sub-request + self.demand[fm_amod_rid_struct].receive_offer(amod_op_id, fm_amod_offer, None) + + # create PT sub-request and inform PT operator + transfer_station_ids: tp.List[str] = parent_rq_obj.get_transfer_station_ids() + transfer_street_node, _ = self._find_transfer_info(transfer_station_ids[0], "pt2street") + # determine the earliest start time of the PT sub-request based on the FM AMoD offer + fm_est_pt_mod: int = self._determine_est_pt_mod(parent_rq_obj,amod_op_id, fm_amod_offer) + # inform PT operator + fm_pt_arrival: tp.Optional[int] = self._inform_pt_sub_request( + parent_rq_obj, + RQ_SUB_TRIP_ID.FM_PT.value, + transfer_street_node, + parent_rq_obj.get_destination_node(), + fm_est_pt_mod, + parent_modal_state, + amod_op_id, + ) + fm_pt_offer: 'TravellerOffer' = self.pt_operator.get_current_offer(fm_pt_rid_struct, amod_op_id) + # check if PT offer is available + if fm_pt_arrival is None or fm_pt_offer is None or fm_pt_offer.service_declined(): + LOG.info(f"PT offer is not available for sub_request {fm_pt_rid_struct}, skipping to next AMoD operator.") + continue + # register the PT offer in the sub-request + self.demand[fm_pt_rid_struct].receive_offer(self.pt_operator_id, fm_pt_offer, None) + + # create intermodal offer + sub_trip_offers: tp.Dict[int, TravellerOffer] = {} + sub_trip_offers[RQ_SUB_TRIP_ID.FM_AMOD.value] = fm_amod_offer + sub_trip_offers[RQ_SUB_TRIP_ID.FM_PT.value] = fm_pt_offer + intermodal_offer: 'IntermodalOffer' = self._create_intermodal_offer(rid, sub_trip_offers, parent_modal_state) + LOG.info(f"Created intermodal offer for request {rid}: {intermodal_offer}") + + # update FM latest dropoff time based on the PT offer + sub_prq_obj: 'PlanRequest' = self.amod_operators[amod_op_id].rq_dict[fm_amod_rid_struct] + old_t_do_latest: int = sub_prq_obj.t_do_latest + new_t_do_latest: int = self._determine_amod_latest_dropoff_time(parent_rq_obj, fm_amod_offer, fm_pt_offer.get(G_OFFER_WAIT), old_t_do_latest) + sub_prq_obj.set_new_dropoff_time_constraint(new_t_do_latest) + + # add intermodal offer to offers dictionary + offers[intermodal_offer.operator_id] = intermodal_offer + + return offers + + def _process_collect_lastmile_offers(self, rid: int, parent_modal_state: RQ_MODAL_STATE, offers: tp.Dict[int, 'TravellerOffer']) -> tp.Dict[int, 'TravellerOffer']: + """This method processes the collection of LM offers. + """ + # get LM PT offer + lm_pt_rid_struct: str = f"{rid}_{RQ_SUB_TRIP_ID.LM_PT.value}" + lm_pt_offer: 'TravellerOffer' = self.pt_operator.get_current_offer(lm_pt_rid_struct) + LOG.debug(f"Collecting lm_pt offer for request {lm_pt_rid_struct} from PT operator {self.pt_operator_id}: {lm_pt_offer}") + + if lm_pt_offer is not None and not lm_pt_offer.service_declined(): + # register the PT offer in the sub-request + self.demand[lm_pt_rid_struct].receive_offer(self.pt_operator_id, lm_pt_offer, None) + + # get LM AMoD offers + lm_amod_rid_struct: str = f"{rid}_{RQ_SUB_TRIP_ID.LM_AMOD.value}" + for amod_op_id in range(self.n_amod_op): + lm_amod_offer = self.amod_operators[amod_op_id].get_current_offer(lm_amod_rid_struct) + LOG.debug(f"Collecting lm_amod offer for request {lm_amod_rid_struct} from operator {amod_op_id}: {lm_amod_offer}") + + if lm_amod_offer is not None and not lm_amod_offer.service_declined(): + # register the LM AMoD offer in the sub-request + self.demand[lm_amod_rid_struct].receive_offer(amod_op_id, lm_amod_offer, None) + + # create intermodal offer + sub_trip_offers: tp.Dict[int, 'TravellerOffer'] = {} + sub_trip_offers[RQ_SUB_TRIP_ID.LM_PT.value] = lm_pt_offer + sub_trip_offers[RQ_SUB_TRIP_ID.LM_AMOD.value] = lm_amod_offer + intermodal_offer: 'IntermodalOffer' = self._create_intermodal_offer(rid, sub_trip_offers, parent_modal_state) + offers[intermodal_offer.operator_id] = intermodal_offer + else: + LOG.info(f"AMoD offer is not available for sub_request {lm_amod_rid_struct}, skipping to next AMoD operator.") + else: + LOG.info(f"PT offer is not available for sub_request {lm_pt_rid_struct}") + return offers + + def _process_collect_firstlastmile_offers( + self, rid: int, parent_rq_obj: 'BasicIntermodalRequest', parent_modal_state: RQ_MODAL_STATE, + offers: tp.Dict[int, 'TravellerOffer'], sim_time: int + ) -> tp.Dict[int, 'TravellerOffer']: + """This method processes the collection of firstlastmile offers. + """ + # get rid struct for all sections + flm_amod_rid_struct_0: str = f"{rid}_{RQ_SUB_TRIP_ID.FLM_AMOD_0.value}" + flm_pt_rid_struct: str = f"{rid}_{RQ_SUB_TRIP_ID.FLM_PT.value}" + flm_amod_rid_struct_1: str = f"{rid}_{RQ_SUB_TRIP_ID.FLM_AMOD_1.value}" + + for amod_op_id in range(self.n_amod_op): + # collect FM AMoD offer + flm_amod_offer_0: 'TravellerOffer' = self.amod_operators[amod_op_id].get_current_offer(flm_amod_rid_struct_0) + LOG.debug(f"Collecting flm_amod_0 offer for request {flm_amod_rid_struct_0} from operator {amod_op_id}: {flm_amod_offer_0}.") + + if flm_amod_offer_0 is None or flm_amod_offer_0.service_declined(): + LOG.info(f"FM AMoD offer is not available for sub_request {flm_amod_rid_struct_0}, skipping to next AMoD operator.") + continue + # register the FM AMoD offer in the sub-request + self.demand[flm_amod_rid_struct_0].receive_offer(amod_op_id, flm_amod_offer_0, None) + + # create PT sub-request and inform PT operator + # get the transfer station ids and their closest pt stations + transfer_station_ids: tp.List[str] = parent_rq_obj.get_transfer_station_ids() + transfer_street_node_0, _ = self._find_transfer_info(transfer_station_ids[0], "pt2street") + transfer_street_node_1, _ = self._find_transfer_info(transfer_station_ids[1], "pt2street") + # determine the earliest start time of the PT sub-request based on the FM AMoD offer + flm_est_pt_mod: int = self._determine_est_pt_mod(parent_rq_obj, amod_op_id, flm_amod_offer_0) + # inform PT operator + flm_pt_arrival: tp.Optional[int] = self._inform_pt_sub_request( + parent_rq_obj, + RQ_SUB_TRIP_ID.FLM_PT.value, + transfer_street_node_0, + transfer_street_node_1, + flm_est_pt_mod, + parent_modal_state, + amod_op_id, + ) + flm_pt_offer: 'TravellerOffer' = self.pt_operator.get_current_offer(flm_pt_rid_struct, amod_op_id) + # check if PT offer is available + if flm_pt_arrival is None or flm_pt_offer is None or flm_pt_offer.service_declined(): + LOG.info(f"PT offer is not available for sub_request {flm_pt_rid_struct}, skipping to next AMoD operator.") + continue + # register the PT offer in the sub-request + self.demand[flm_pt_rid_struct].receive_offer(self.pt_operator_id, flm_pt_offer, None) + + # create LM AMoD sub-request and inform AMoD operator + self._inform_amod_sub_request( + parent_rq_obj, + RQ_SUB_TRIP_ID.FLM_AMOD_1.value, + transfer_street_node_1, + parent_rq_obj.get_destination_node(), + flm_pt_arrival, + parent_modal_state, + amod_op_id, + sim_time, + ) + flm_amod_offer_1: 'TravellerOffer' = self.amod_operators[amod_op_id].get_current_offer(flm_amod_rid_struct_1) + # check if LM AMoD offer is available + if flm_amod_offer_1 is None or flm_amod_offer_1.service_declined(): + LOG.info(f"LM AMoD offer is not available for sub_request {flm_amod_rid_struct_1}, skipping to next AMoD operator.") + continue + # register the LM AMoD offer in the sub-request + self.demand[flm_amod_rid_struct_1].receive_offer(amod_op_id, flm_amod_offer_1, None) + + # create intermodal offer + sub_trip_offers: tp.Dict[int, 'TravellerOffer'] = {} + sub_trip_offers[RQ_SUB_TRIP_ID.FLM_AMOD_0.value] = flm_amod_offer_0 + sub_trip_offers[RQ_SUB_TRIP_ID.FLM_PT.value] = flm_pt_offer + sub_trip_offers[RQ_SUB_TRIP_ID.FLM_AMOD_1.value] = flm_amod_offer_1 + intermodal_offer: 'IntermodalOffer' = self._create_intermodal_offer(rid, sub_trip_offers, parent_modal_state) + LOG.info(f"Created intermodal offer for request {rid}: {intermodal_offer}") + + # update FM latest dropoff time based on the PT offer + sub_prq_obj: 'PlanRequest' = self.amod_operators[amod_op_id].rq_dict[flm_amod_rid_struct_0] + old_t_do_latest: int = sub_prq_obj.t_do_latest + new_t_do_latest: int = self._determine_amod_latest_dropoff_time(parent_rq_obj, flm_amod_offer_0, flm_pt_offer.get(G_OFFER_WAIT), old_t_do_latest) + sub_prq_obj.set_new_dropoff_time_constraint(new_t_do_latest) + + # add intermodal offer to offers dictionary + offers[intermodal_offer.operator_id] = intermodal_offer + + return offers + + def _determine_est_pt_mod(self, rq_obj: 'RequestBase', amod_op_id: int, amod_offer: 'TravellerOffer') -> int: + """This method determines the earliest start time for the pt sub-request. + """ + t_est_pt_mod: int = rq_obj.earliest_start_time + amod_offer.get(G_OFFER_WAIT) + amod_offer.get(G_OFFER_DRIVE) + self.amod_operators[amod_op_id].const_bt + return t_est_pt_mod + + def _determine_amod_latest_dropoff_time(self, rq_obj: 'RequestBase', amod_offer: 'TravellerOffer', pt_waiting_time: int, old_t_do_latest: int) -> tp.Optional[int]: + """This method determines the latest dropoff time for the amod sub-request. + """ + t_do_latest: int = rq_obj.earliest_start_time + amod_offer.get(G_OFFER_WAIT) + amod_offer.get(G_OFFER_DRIVE) + pt_waiting_time + # add latest dropoff time constraint check + if t_do_latest > old_t_do_latest: + t_do_latest = old_t_do_latest + return t_do_latest \ No newline at end of file diff --git a/src/broker/PTBrokerBasic.py b/src/broker/PTBrokerBasic.py new file mode 100644 index 00000000..9bb56fc7 --- /dev/null +++ b/src/broker/PTBrokerBasic.py @@ -0,0 +1,595 @@ +# TODO: +# - Adjust PT waiting time based on dynamic GTFS data (e.g., delays), and then adjust FM and LM offers accordingly. +# - Support multiple AMoD operators for firstlastmile requests. + +# -------------------------------------------------------------------------------------------------------------------- # +# PTBrokerBasic: Base Intermodal Broker +# +# Base class for all PTBroker variants. Provides the shared infrastructure for intermodal request handling: +# - MONOMODAL / PT: Forward the request directly to the AMoD operator or PT operator. +# - FIRSTMILE (FM): Select a PT boarding transfer stop, create FM_AMOD sub-request +# (origin → PT boarding stop), and create PT sub-request (boarding stop → destination). +# - LASTMILE (LM): Select a PT alighting transfer stop, create PT sub-request +# (origin → PT alighting stop), and create LM_AMOD sub-request (PT alighting stop → destination). +# - FIRSTLASTMILE (FLM): Select boarding and alighting transfer stops, create FLM_AMOD_0 sub-request +# (origin → PT boarding stop), PT sub-request (boarding → alighting), and FLM_AMOD_1 sub-request +# (PT alighting stop → destination). +# Subclasses (PTBroker, PTBrokerEI, PTBrokerPAYG) override specific methods to implement different +# coordination strategies between MaaS and DRT. +# +# NOTE: This code has only been tested and applied in the ImmediateDecisionsSimulation environment +# combined with the PoolingIRSOnly fleet controller. +# -------------------------------------------------------------------------------------------------------------------- # + +# -------------------------------------------------------------------------------------------------------------------- # +# standard distribution imports +# ----------------------------- +import logging +from datetime import datetime, timedelta +import typing as tp +import pandas as pd +# additional module imports (> requirements) +# ------------------------------------------ + + +# src imports +# ----------- +from src.broker.BrokerBasic import BrokerBasic +from src.simulation.Offers import IntermodalOffer +if tp.TYPE_CHECKING: + from src.fleetctrl.FleetControlBase import FleetControlBase + from src.fleetctrl.planning.PlanRequest import PlanRequest + from src.ptctrl.PTControlBase import PTControlBase + from src.demand.demand import Demand + from src.routing.road.NetworkBase import NetworkBase + from src.demand.TravelerModels import RequestBase, BasicIntermodalRequest + from src.simulation.Offers import TravellerOffer, PTOffer + +# -------------------------------------------------------------------------------------------------------------------- # +# global variables +# ---------------- +from src.misc.globals import * + +LOG = logging.getLogger(__name__) +LARGE_INT = 100000000 +BUFFER_SIZE = 100 + +INPUT_PARAMETERS_PTBroker = { + "doc" : "this class represents a broker platform which handles intermodal requests", + "inherit" : BrokerBasic, + "input_parameters_mandatory": ["n_amod_op", "amod_operators", "pt_operator", "demand", "routing_engine", "scenario_parameters"], + "input_parameters_optional": [], + "mandatory_modules": [], + "optional_modules": [] +} + +# -------------------------------------------------------------------------------------------------------------------- # +# main +# ---- +class PTBrokerBasic(BrokerBasic): + def __init__( + self, + n_amod_op: int, + amod_operators: tp.List['FleetControlBase'], + pt_operator: 'PTControlBase', + demand: 'Demand', + routing_engine: 'NetworkBase', + scenario_parameters: dict, + ): + """ + The general attributes for the broker are initialized. + + Args: + n_amod_op (int): number of AMoD operators + amod_operators (tp.List['FleetControlBase']): list of AMoD operators + pt_operator (PTControlBase): PT operator + demand (Demand): demand object + routing_engine (NetworkBase): routing engine + scenario_parameters (dict): scenario parameters + """ + super().__init__(n_amod_op, amod_operators) + + self.demand: Demand = demand + self.routing_engine: NetworkBase = routing_engine + self.pt_operator: PTControlBase = pt_operator + + self.pt_operator_id: int = self.pt_operator.pt_operator_id + self.scenario_parameters: dict = scenario_parameters + + # set whether to always query pure PT offers + self.always_query_pt: bool = self.scenario_parameters.get(G_BROKER_ALWAYS_QUERY_PT, False) + + # set simulation start date for Raptor routing + self.sim_start_datetime: datetime = None + self._set_sim_start_datetime(self.scenario_parameters.get(G_PT_SIM_START_DATE, None)) + + # method for finding transfer stations + self.transfer_search_method: str = self.scenario_parameters.get(G_BROKER_TRANSFER_SEARCH_METHOD, "closest") + # read necessary files based on the transfer search method + # default method: closest transfer station search. + if self.transfer_search_method == "closest": + # load the street-station transfers: used for finding closest station to a street node, or vice versa + try: + self.street_station_transfers_fp_df = self._load_street_station_transfers_from_gtfs(self.pt_operator.gtfs_dir) + except FileNotFoundError: + LOG.error("PTBroker: street_station_transfers_fp.txt file not found in the GTFS directory, which is required for finding closest transfer stations!") + raise FileNotFoundError("PTBroker: street_station_transfers_fp.txt file not found in the GTFS directory, which is required for finding closest transfer stations!") + + + def inform_request(self, rid: int, rq_obj: 'RequestBase', sim_time: int): + """This method informs the broker that a new request has been made. + Based on the request modal state, the broker will create the appropriate sub-requests + and inform the operators. + + Args: + rid (int): parent request id + rq_obj (RequestBase): request object + sim_time (int): simulation time + """ + parent_modal_state: RQ_MODAL_STATE = rq_obj.get_modal_state() + LOG.debug(f"inform request: {rid} at sim time {sim_time} with modal state {parent_modal_state}; query pure PT offer: {self.always_query_pt}") + + if self.always_query_pt: + # 1. query the PT operator for the pure PT travel costs + _ = self._inform_pt_sub_request(rq_obj, RQ_SUB_TRIP_ID.PT.value, rq_obj.get_origin_node(), rq_obj.get_destination_node(), rq_obj.earliest_start_time, parent_modal_state) + + # 2.1 pure AMoD request or PT request + if parent_modal_state == RQ_MODAL_STATE.MONOMODAL or parent_modal_state == RQ_MODAL_STATE.PT: + self._process_inform_monomodal_request(rid, rq_obj, sim_time, parent_modal_state) + + # 2.2 AMoD as firstmile request + elif parent_modal_state == RQ_MODAL_STATE.FIRSTMILE: + self._process_inform_firstmile_request(rid, rq_obj, sim_time, parent_modal_state) + + # 2.3 AMoD as lastmile request + elif parent_modal_state == RQ_MODAL_STATE.LASTMILE: + self._process_inform_lastmile_request(rid, rq_obj, sim_time, parent_modal_state) + + # 2.4 AMoD as firstlastmile request + elif parent_modal_state == RQ_MODAL_STATE.FIRSTLASTMILE: + self._process_inform_firstlastmile_request(rid, rq_obj, sim_time, parent_modal_state) + + else: + raise ValueError(f"Invalid modal state: {parent_modal_state}") + + def collect_offers(self, rid: int, sim_time: int) -> tp.Dict[int, 'TravellerOffer']: + """This method collects the offers from the operators. + + Args: + rid (int): parent request id + sim_time (int): simulation time + Returns: + tp.Dict[int, TravellerOffer]: a dictionary of offers from the operators + """ + # get parent request modal state + parent_rq_obj: RequestBase = self.demand[rid] + parent_modal_state: RQ_MODAL_STATE = parent_rq_obj.get_modal_state() + offers: tp.Dict[int, TravellerOffer] = {} + LOG.debug(f"Collecting offers for request {rid} with modal state {parent_modal_state}") + + # 1. collect PT offers for multimodal requests + if parent_modal_state.value > RQ_MODAL_STATE.MONOMODAL.value or self.always_query_pt: + pt_rid_struct: str = f"{rid}_{RQ_SUB_TRIP_ID.PT.value}" + pt_offer = self.pt_operator.get_current_offer(pt_rid_struct) + LOG.debug(f"pt offer {pt_offer}") + + if pt_offer is not None and not pt_offer.service_declined(): + offers[self.pt_operator_id] = pt_offer + # register the pt offer in the sub-request + self.demand[pt_rid_struct].receive_offer(self.pt_operator_id, pt_offer, None) + + # 2.1 collect AMoD offers for MONOMODAL and PT requests + if parent_modal_state == RQ_MODAL_STATE.MONOMODAL or parent_modal_state == RQ_MODAL_STATE.PT: + offers = self._process_collect_monomodal_offers(rid, parent_modal_state, offers) + + # 2.2 collect AMoD offers for FIRSTMILE requests + elif parent_modal_state == RQ_MODAL_STATE.FIRSTMILE: + offers = self._process_collect_firstmile_offers(rid, parent_rq_obj, parent_modal_state, offers) + + # 2.3 collect AMoD offers for LASTMILE requests + elif parent_modal_state == RQ_MODAL_STATE.LASTMILE: + offers = self._process_collect_lastmile_offers(rid, parent_modal_state, offers) + + # 2.4 collect AMoD offers for FIRSTLASTMILE requests + elif parent_modal_state == RQ_MODAL_STATE.FIRSTLASTMILE: + offers = self._process_collect_firstlastmile_offers(rid, parent_rq_obj, parent_modal_state, offers, sim_time) + + else: + raise ValueError(f"Invalid modal state: {parent_modal_state}") + + return offers + + def inform_user_booking(self, rid: int, rq_obj: 'RequestBase', sim_time: int, chosen_operator: tp.Union[int, tuple]) -> tp.List[tuple[int, 'RequestBase']]: + """This method informs the broker that the user has booked a trip. + """ + amod_confirmed_rids = [] + parent_modal_state: RQ_MODAL_STATE = rq_obj.get_modal_state() + + # 1. Pure PT offer has been selected + if chosen_operator == self.pt_operator_id: + amod_confirmed_rids.append((rid, rq_obj)) + + # inform all AMoD operators that the request is cancelled + self.inform_user_leaving_system(rid, sim_time) + + # inform PT operator that the request is confirmed + pt_rid_struct: str = f"{rid}_{RQ_SUB_TRIP_ID.PT.value}" + pt_sub_rq_obj: BasicIntermodalRequest = self.demand[pt_rid_struct] + self.pt_operator.user_confirms_booking(pt_sub_rq_obj, None) + # 2. AMoD involved offer has been selected + else: + # non-intermodal offer has been selected + if parent_modal_state == RQ_MODAL_STATE.MONOMODAL or parent_modal_state == RQ_MODAL_STATE.PT: + for i, operator in enumerate(self.amod_operators): + if i != chosen_operator: # Non-intermodal requests: the chosen operator has the data type int + operator.user_cancels_request(rid, sim_time) + else: + operator.user_confirms_booking(rid, sim_time) + amod_confirmed_rids.append((rid, rq_obj)) + # intermodal offer has been selected + elif parent_modal_state.value > RQ_MODAL_STATE.MONOMODAL.value and parent_modal_state.value < RQ_MODAL_STATE.PT.value: + # chosen_operator has the data type tuple: ((operator_id, sub_trip_id), ...) + for operator_id, sub_trip_id in chosen_operator: + if operator_id == self.pt_operator_id: + # inform the pt operator that the request is confirmed + pt_rid_struct: str = f"{rid}_{sub_trip_id}" + pt_sub_rq_obj: BasicIntermodalRequest = self.demand[pt_rid_struct] + + if parent_modal_state == RQ_MODAL_STATE.LASTMILE: + previous_amod_operator_id = None # no previous amod operator + else: # firstmile or firstlastmile + previous_amod_operator_id: int = chosen_operator[0][0] # the first amod operator + self.pt_operator.user_confirms_booking(pt_sub_rq_obj, previous_amod_operator_id) + else: + # inform the amod operator that the request is confirmed + amod_rid_struct: str = f"{rid}_{sub_trip_id}" + for i, operator in enumerate(self.amod_operators): + if i != operator_id: + operator.user_cancels_request(amod_rid_struct, sim_time) + else: + operator.user_confirms_booking(amod_rid_struct, sim_time) + amod_confirmed_rids.append((rid, rq_obj)) + else: + raise ValueError(f"Invalid modal state: {parent_modal_state}") + return amod_confirmed_rids + + def inform_user_leaving_system(self, rid: int, sim_time: int): + """This method informs the broker that the user is leaving the system. + """ + rq_obj: RequestBase = self.demand[rid] + parent_modal_state: RQ_MODAL_STATE = rq_obj.get_modal_state() + + if parent_modal_state == RQ_MODAL_STATE.MONOMODAL or parent_modal_state == RQ_MODAL_STATE.PT: + for _, operator in enumerate(self.amod_operators): + operator.user_cancels_request(rid, sim_time) + + elif parent_modal_state == RQ_MODAL_STATE.FIRSTMILE: + fm_amod_rid_struct: str = f"{rid}_{RQ_SUB_TRIP_ID.FM_AMOD.value}" + for _, operator in enumerate(self.amod_operators): + operator.user_cancels_request(fm_amod_rid_struct, sim_time) + + elif parent_modal_state == RQ_MODAL_STATE.LASTMILE: + lm_amod_rid_struct: str = f"{rid}_{RQ_SUB_TRIP_ID.LM_AMOD.value}" + for _, operator in enumerate(self.amod_operators): + try: + operator.user_cancels_request(lm_amod_rid_struct, sim_time) + except KeyError: + # LM AMoD sub-request may not be created if no PT offer is available + LOG.info(f"LM AMoD sub-request {lm_amod_rid_struct} not found when user leaves system, possibly no PT offer available so the LM sub-request was not created.") + + elif parent_modal_state == RQ_MODAL_STATE.FIRSTLASTMILE: + flm_amod_rid_struct_0: str = f"{rid}_{RQ_SUB_TRIP_ID.FLM_AMOD_0.value}" + flm_amod_rid_struct_1: str = f"{rid}_{RQ_SUB_TRIP_ID.FLM_AMOD_1.value}" + for _, operator in enumerate(self.amod_operators): + operator.user_cancels_request(flm_amod_rid_struct_0, sim_time) + try: + operator.user_cancels_request(flm_amod_rid_struct_1, sim_time) + except KeyError: + # LM AMoD sub-request may not be created if no PT offer is available + LOG.info(f"LM AMoD sub-request {flm_amod_rid_struct_1} not found when user leaves system, possibly no FM or PT offer available so the LM sub-request was not created.") + + else: + raise ValueError(f"Invalid modal state: {parent_modal_state}") + + def inform_waiting_request_cancellations(self, chosen_operator: int, rid: int, sim_time: int): + """This method informs the operators that the waiting requests have been cancelled. + """ + rq_obj: RequestBase = self.demand[rid] + parent_modal_state: RQ_MODAL_STATE = rq_obj.get_modal_state() + + if chosen_operator == self.pt_operator_id: + return + + if parent_modal_state == RQ_MODAL_STATE.MONOMODAL or parent_modal_state == RQ_MODAL_STATE.PT: + self.amod_operators[chosen_operator].user_cancels_request(rid, sim_time) + + elif parent_modal_state.value > RQ_MODAL_STATE.MONOMODAL.value and parent_modal_state.value < RQ_MODAL_STATE.PT.value: + for operator_id, sub_trip_id in chosen_operator: + if operator_id == self.pt_operator_id: + continue + amod_rid_struct: str = f"{rid}_{sub_trip_id}" + operator_id = int(operator_id) + self.amod_operators[operator_id].user_cancels_request(amod_rid_struct, sim_time) + + else: + raise ValueError(f"Invalid modal state: {parent_modal_state}") + + def _process_inform_monomodal_request(self, rid: int, rq_obj: 'RequestBase', sim_time: int, parent_modal_state: RQ_MODAL_STATE,): + """This method processes the new monomodal request. + + Args: + rid (int): the request id + rq_obj ('RequestBase'): the request object + sim_time (int): the simulation time + parent_modal_state (RQ_MODAL_STATE): the parent modal state + """ + for op_id in range(self.n_amod_op): + LOG.debug(f"AMoD Request {rid} with modal state {parent_modal_state}: To operator {op_id} ...") + self.amod_operators[op_id].user_request(rq_obj, sim_time) + + def _process_inform_firstmile_request(self, rid: int, rq_obj: 'BasicIntermodalRequest', sim_time: int, parent_modal_state: RQ_MODAL_STATE = RQ_MODAL_STATE.FIRSTMILE): + """This method processes the new firstmile request. + In this stage, only the first-mile AMoD sub-request is created first; the PT sub-request will be created after receiving the AMoD offer. + + Args: + rid (int): the request id + rq_obj ('BasicIntermodalRequest'): the request object + sim_time (int): the simulation time + parent_modal_state (RQ_MODAL_STATE): the parent modal state + """ + pass + + def _process_inform_lastmile_request(self, rid: int, rq_obj: 'BasicIntermodalRequest', sim_time: int, parent_modal_state: RQ_MODAL_STATE = RQ_MODAL_STATE.LASTMILE): + """This method processes the new lastmile request. + First, the PT sub-request is created. If the PT offer is available, then the last-mile AMoD sub-request is created. + + Args: + rid (int): the request id + rq_obj ('BasicIntermodalRequest'): the request object + sim_time (int): the simulation time + parent_modal_state (RQ_MODAL_STATE): the parent modal state + """ + pass + + def _process_inform_firstlastmile_request(self, rid: int, rq_obj: 'BasicIntermodalRequest', sim_time: int, parent_modal_state: RQ_MODAL_STATE = RQ_MODAL_STATE.FIRSTLASTMILE): + """This method processes the new firstlastmile request. + In this stage, only the first-mile AMoD sub-request is created first; the PT and last-mile AMoD sub-requests will be created after receiving the first-mile AMoD offer. + + Args: + rid (int): the request id + rq_obj ('BasicIntermodalRequest'): the request object + sim_time (int): the simulation time + parent_modal_state (RQ_MODAL_STATE): the parent modal state + """ + pass + + def _inform_amod_sub_request( + self, rq_obj: 'RequestBase', sub_trip_id: int, leg_o_node: int, leg_d_node: int, leg_start_time: int, + parent_modal_state: RQ_MODAL_STATE, op_id: int, sim_time: int + ): + """ + This method informs the AMoD operators that a new sub-request has been made. + + Args: + rq_obj ('RequestBase'): the parent request object + sub_trip_id (int): the sub-trip id + leg_o_node (int): the origin node of the sub-request + leg_d_node (int): the destination node of the sub-request + leg_start_time (int): the start time of the sub-request + parent_modal_state (RQ_MODAL_STATE): the parent modal state + """ + amod_sub_rq_obj: RequestBase = self.demand.create_sub_requests(rq_obj, sub_trip_id, leg_o_node, leg_d_node, leg_start_time, parent_modal_state) + LOG.debug(f"AMoD sub-request {amod_sub_rq_obj.get_rid_struct()} with modal state {parent_modal_state}: To operator {op_id} ...") + + self.amod_operators[op_id].user_request(amod_sub_rq_obj, sim_time) + + def _inform_pt_sub_request( + self, rq_obj: 'RequestBase', sub_trip_id: int, leg_o_node: int, leg_d_node: int, leg_start_time: int, + parent_modal_state: RQ_MODAL_STATE, firstmile_amod_operator_id: int = None + ) -> tp.Optional[int]: + """ + This method informs the PT operator that a new sub-request has been made. + + Args: + rq_obj (RequestBase): the parent request object + sub_trip_id (int): the sub_trip id + leg_o_node (int): the origin street node of the sub-request + leg_d_node (int): the destination street node of the sub-request + leg_start_time (int): the start time [s] of the sub-request at the origin street node + parent_modal_state (RQ_MODAL_STATE): the parent modal state + firstmile_amod_operator_id (int): the id of the firstmile amod operator, only used for FM and FLM requests + Returns: + t_d_node_arrival (tp.Optional[int]): + the pt arrival time of the sub-request at the destination street node + or None if the pt travel costs are not available + """ + pt_sub_rq_obj: RequestBase = self.demand.create_sub_requests(rq_obj, sub_trip_id, leg_o_node, leg_d_node, leg_start_time, parent_modal_state) + LOG.debug(f"PT sub-request {pt_sub_rq_obj.get_rid_struct()} with modal state {parent_modal_state}: To PT operator {self.pt_operator_id} ...") + + costs_info = self._query_street_node_pt_travel_costs_1to1( + pt_sub_rq_obj.get_origin_node(), + pt_sub_rq_obj.get_destination_node(), + pt_sub_rq_obj.earliest_start_time, + pt_sub_rq_obj.get_max_transfers(), # the request type should be BasicIntermodalRequest + ) + + if costs_info is not None: + source_pt_station_id, t_source_walk, target_pt_station_id, t_target_walk, pt_journey_plan_dict = costs_info + LOG.debug(f"PT sub-request {pt_sub_rq_obj.get_rid_struct()} with modal state {parent_modal_state}: Found offer with source_pt_station_id {source_pt_station_id}, t_source_walk {t_source_walk}, target_pt_station_id {target_pt_station_id}, t_target_walk {t_target_walk}, pt_journey_plan_dict {pt_journey_plan_dict}") + else: + source_pt_station_id = None + t_source_walk = None + target_pt_station_id = None + t_target_walk = None + pt_journey_plan_dict = None + LOG.debug(f"PT sub-request {pt_sub_rq_obj.get_rid_struct()} with modal state {parent_modal_state}: No PT offer has been found!") + + pt_rid_struct: str = pt_sub_rq_obj.get_rid_struct() + + self.pt_operator.create_and_record_pt_offer_db( + rid_struct = pt_rid_struct, + operator_id = self.pt_operator_id, + source_station_id = source_pt_station_id, + target_station_id = target_pt_station_id, + source_walking_time = t_source_walk, + target_walking_time = t_target_walk, + pt_journey_plan_dict = pt_journey_plan_dict, + firstmile_amod_operator_id = firstmile_amod_operator_id, + ) + if pt_journey_plan_dict is not None: + t_d_node_arrival: int = self.pt_operator.get_current_offer(pt_rid_struct, firstmile_amod_operator_id).destination_node_arrival_time # Offer type: PTOffer + return t_d_node_arrival + else: + return None + + def _process_collect_monomodal_offers(self, rid: int, parent_modal_state: RQ_MODAL_STATE, offers: tp.Dict[int, 'TravellerOffer']) -> tp.Dict[int, 'TravellerOffer']: + """This method processes the collection of monomodal offers. + + Args: + rid (int): the request id + parent_modal_state (RQ_MODAL_STATE): the parent modal state + offers (tp.Dict[int, TravellerOffer]): the current offers dictionary + Returns: + tp.Dict[int, TravellerOffer]: the updated offers dictionary + """ + for amod_op_id in range(self.n_amod_op): + amod_offer = self.amod_operators[amod_op_id].get_current_offer(rid) + LOG.debug(f"Collecting amod offer for request {rid} with modal state {parent_modal_state} from operator {amod_op_id}: {amod_offer}") + if amod_offer is not None and not amod_offer.service_declined(): + offers[amod_op_id] = amod_offer + return offers + + def _process_collect_firstmile_offers( + self, rid: int, parent_rq_obj: 'BasicIntermodalRequest', parent_modal_state: RQ_MODAL_STATE, + offers: tp.Dict[int, 'TravellerOffer'] + ) -> tp.Dict[int, 'TravellerOffer']: + """This method processes the collection of firstmile offers and try to optimize the waiting time of the PT leg. + """ + # get rid struct for all sections + pass + + def _process_collect_lastmile_offers(self, rid: int, parent_modal_state: RQ_MODAL_STATE, offers: tp.Dict[int, 'TravellerOffer']) -> tp.Dict[int, 'TravellerOffer']: + """This method processes the collection of LM offers. + """ + pass + + def _process_collect_firstlastmile_offers( + self, rid: int, parent_rq_obj: 'BasicIntermodalRequest', parent_modal_state: RQ_MODAL_STATE, + offers: tp.Dict[int, 'TravellerOffer'], sim_time: int + ) -> tp.Dict[int, 'TravellerOffer']: + """This method processes the collection of firstlastmile offers. + """ + pass + + def _set_sim_start_datetime(self, sim_start_date: str): + """This method sets the simulation start date. + Converts the date string (format YYYYMMDD) to a datetime object. + + Args: + sim_start_date (str): the simulation start date in format YYYYMMDD + """ + if sim_start_date is None: + LOG.error("PTBrokerTPCS: Simulation start date for PT routing not provided in scenario parameters!") + raise ValueError("PTBrokerTPCS: Simulation start date for PT routing not provided in scenario parameters!") + + if type(sim_start_date) is not str: + sim_start_date = str(int(sim_start_date)) + self.sim_start_datetime = datetime.strptime(sim_start_date, "%Y%m%d") + + def _get_current_datetime(self, sim_time_in_seconds: int) -> datetime: + """This method returns the current datetime based on the simulation time in seconds. + """ + return self.sim_start_datetime + timedelta(seconds=int(sim_time_in_seconds)) + + def _load_street_station_transfers_from_gtfs(self, gtfs_dir: str) -> pd.DataFrame: + """This method loads the FleetPy-specific street station transfers file. + + Args: + gtfs_dir (str): The directory containing the GTFS data of the operator. + Returns: + pd.DataFrame: The transfer data between the street nodes and the PT stations. + """ + dtypes = { + 'node_id': 'int', + 'closest_station_id': 'str', + 'street_station_transfer_time': 'int', + } + return pd.read_csv(os.path.join(gtfs_dir, "street_station_transfers_fp.txt"), dtype=dtypes) + + def _query_street_node_pt_travel_costs_1to1( + self, o_node: int, d_node: int, est: int, + max_transfers: int = 999, detailed: bool = False + ) -> tp.Optional[tp.Tuple[int, int, int, int, tp.Dict[str, tp.Any]]]: + """This method queries the pt travel costs between two street nodes at a given datetime. + The pt station ids will be the closest pt station ids to the street nodes. + + Args: + o_node (int): The origin street node id. + d_node (int): The destination street node id. + est (int): The earliest start time of the request at the origin street node in seconds. + max_transfers (int): The maximum number of transfers allowed in the journey, 999 for no limit. + detailed (bool): Whether to return detailed journey information. Defaults to False. + Returns: + tp.Optional[tp.Tuple[int, int, int, int, tp.Dict[str, tp.Any]]]: + Returns a tuple containing: + (source_pt_station_id, t_source_walking, target_pt_station_id, t_target_walking, pt_journey_plan_dict) + if a public transport journey plan is found. + Returns None if no public transport journey plan is available. + """ + # find pt transfer stations + source_pt_station_id, t_source_walk = self._find_transfer_info(o_node, "street2pt") + target_pt_station_id, t_target_walk = self._find_transfer_info(d_node, "street2pt") + + source_station_departure_seconds: int = est + t_source_walk + source_station_departure_datetime: datetime = self._get_current_datetime(source_station_departure_seconds) + LOG.debug(f"Query PT travel costs: {o_node} -> {d_node} (stations: {source_pt_station_id} -> {target_pt_station_id}) at {est} (station departure: {source_station_departure_datetime})") + pt_journey_plan_dict: tp.Union[tp.Dict[str, tp.Any], None] = self.pt_operator.return_fastest_pt_journey_1to1( + source_pt_station_id, target_pt_station_id, + source_station_departure_datetime, + max_transfers, detailed, + ) + if pt_journey_plan_dict is None: + return None + else: + return source_pt_station_id, t_source_walk, target_pt_station_id, t_target_walk, pt_journey_plan_dict + + def _find_transfer_info(self, node_id: tp.Union[int, str], direction: str) -> tp.Tuple[str, int]: + """This method finds the transfer possibility between pt station and street node. + + Args: + node_id (tp.Union[int, str]): The street node id. + direction (str): "pt2street" or "street2pt" + Returns: + tp.Tuple[str, int]: The transfer node id and the walking time. + """ + if self.transfer_search_method == "closest": # closest station or street node + if direction == "street2pt": # find closest pt station from street node + street_node_id: int = int(node_id) + # TODO: allow multiple closest stations? + street_station_transfer = self.street_station_transfers_fp_df[self.street_station_transfers_fp_df["node_id"] == street_node_id] + if street_station_transfer.empty: + raise ValueError(f"Street node id {street_node_id} not found in the street station transfers file") + closest_station_id: str = street_station_transfer["closest_station_id"].iloc[0] + walking_time: int = street_station_transfer["street_station_transfer_time"].iloc[0] + return closest_station_id, walking_time + elif direction == "pt2street": # find closest street node from pt station + pt_station_id: str = str(node_id) + street_station_transfers = self.street_station_transfers_fp_df[self.street_station_transfers_fp_df["closest_station_id"] == pt_station_id] + if street_station_transfers.empty: + raise ValueError(f"PT station id {pt_station_id} not found in the street station transfers file") + # find the record with the minimum street_station_transfer_time + # TODO: if multiple exist, return all? + min_transfer = street_station_transfers.loc[street_station_transfers["street_station_transfer_time"].idxmin()] + closest_street_node_id: int = min_transfer["node_id"] + walking_time: int = min_transfer["street_station_transfer_time"] + return closest_street_node_id, walking_time + else: + raise ValueError(f"Invalid direction: {direction}. Must be 'pt2street' or 'street2pt'.") + else: + LOG.debug(f"PTBrokerTPCS: Transfer search method '{self.transfer_search_method}' not implemented. Using 'closest' instead.") + raise NotImplementedError(f"PTBrokerTPCS: Transfer search method '{self.transfer_search_method}' not implemented.") + + def _create_intermodal_offer(self, rid: int, sub_trip_offers: tp.Dict[int, 'TravellerOffer'], rq_modal_state: RQ_MODAL_STATE) -> 'IntermodalOffer': + """This method merges the amod and pt offers into an intermodal offer. + """ + return IntermodalOffer(rid, sub_trip_offers, rq_modal_state) \ No newline at end of file diff --git a/src/broker/PTBrokerEI.py b/src/broker/PTBrokerEI.py new file mode 100644 index 00000000..54cf1b66 --- /dev/null +++ b/src/broker/PTBrokerEI.py @@ -0,0 +1,435 @@ +# -------------------------------------------------------------------------------------------------------------------- # +# PTBrokerEI: Estimation-based Integration Strategy +# +# Simulates current MaaS platforms with limited real-time DRT communication: +# - MaaS cannot obtain an actual FM DRT offer in time, so it *estimates* the FM DRT dropoff time +# using a detour time factor (`broker_maas_dtf`). +# - PT and LM DRT are booked based on this estimated dropoff time. +# - `broker_maas_dtf` controls estimation conservatism: +# - A large (conservative) value ensures the user catches PT but increases overall travel time. +# - A small (optimistic) value reduces travel time but risks missing PT due to pooling delays. +# - In experiments, only `broker_maas_dtf=100` was used (50 was not run). +# +# NOTE: This code has only been tested and applied in the ImmediateDecisionsSimulation environment +# combined with the PoolingIRSOnly fleet controller. +# -------------------------------------------------------------------------------------------------------------------- # + +# -------------------------------------------------------------------------------------------------------------------- # +# standard distribution imports +# ----------------------------- +import logging +from datetime import datetime, timedelta +import typing as tp +import pandas as pd +# additional module imports (> requirements) +# ------------------------------------------ + + +# src imports +# ----------- +from src.broker.PTBrokerBasic import PTBrokerBasic +from src.simulation.Offers import IntermodalOffer +if tp.TYPE_CHECKING: + from src.fleetctrl.FleetControlBase import FleetControlBase + from src.fleetctrl.planning.PlanRequest import PlanRequest + from src.ptctrl.PTControlBase import PTControlBase + from src.demand.demand import Demand + from src.routing.road.NetworkBase import NetworkBase + from src.demand.TravelerModels import RequestBase, BasicIntermodalRequest + from src.simulation.Offers import TravellerOffer, PTOffer + +# -------------------------------------------------------------------------------------------------------------------- # +# global variables +# ---------------- +from src.misc.globals import * + +LOG = logging.getLogger(__name__) +LARGE_INT = 100000000 +BUFFER_SIZE = 100 + +INPUT_PARAMETERS_PTBroker = { + "doc" : "this class represents a broker platform which handles intermodal requests", + "inherit" : PTBrokerBasic, + "input_parameters_mandatory": ["n_amod_op", "amod_operators", "pt_operator", "demand", "routing_engine", "scenario_parameters"], + "input_parameters_optional": [], + "mandatory_modules": [], + "optional_modules": [] +} + +# -------------------------------------------------------------------------------------------------------------------- # +# main +# ---- +class PTBrokerEI(PTBrokerBasic): + def __init__( + self, + n_amod_op: int, + amod_operators: tp.List['FleetControlBase'], + pt_operator: 'PTControlBase', + demand: 'Demand', + routing_engine: 'NetworkBase', + scenario_parameters: dict, + ): + """ + The general attributes for the broker are initialized. + + Args: + n_amod_op (int): number of AMoD operators + amod_operators (tp.List['FleetControlBase']): list of AMoD operators + pt_operator (PTControlBase): PT operator + demand (Demand): demand object + routing_engine (NetworkBase): routing engine + scenario_parameters (dict): scenario parameters + """ + super().__init__(n_amod_op, amod_operators, pt_operator, demand, routing_engine, scenario_parameters) + + # set MaaS detour time estimation parameter + self.maas_detour_time_factor: float = self.scenario_parameters.get(G_BROKER_MAAS_DETOUR_TIME_FACTOR , 100) / 100 + + def _process_inform_firstmile_request(self, rid: int, rq_obj: 'BasicIntermodalRequest', sim_time: int, parent_modal_state: RQ_MODAL_STATE = RQ_MODAL_STATE.FIRSTMILE): + """This method processes the new firstmile request. + + Args: + rid (int): the request id + rq_obj ('BasicIntermodalRequest'): the request object + sim_time (int): the simulation time + parent_modal_state (RQ_MODAL_STATE): the parent modal state + """ + # get the transfer station id and its closest pt station + transfer_station_ids: tp.List[str] = rq_obj.get_transfer_station_ids() + transfer_street_node, _ = self._find_transfer_info(transfer_station_ids[0], "pt2street") + + # Make the estimation first, then based on the estimation, create sub-requests + + # create sub-request for AMoD + for op_id in range(self.n_amod_op): + self._inform_amod_sub_request(rq_obj, RQ_SUB_TRIP_ID.FM_AMOD.value, rq_obj.get_origin_node(), transfer_street_node, rq_obj.earliest_start_time, parent_modal_state, op_id, sim_time) + fm_amod_rid_struct: str = f"{rid}_{RQ_SUB_TRIP_ID.FM_AMOD.value}" + fm_amod_sub_rq_obj: BasicIntermodalRequest = self.demand[fm_amod_rid_struct] + # create sub-request for PT + estimated_amod_dropoff_time: int = self._estimate_amod_dropoff_time(op_id, fm_amod_sub_rq_obj) + # estimate the earliest start time of the pt sub-request + fm_est_pt_mod: int = estimated_amod_dropoff_time + self.amod_operators[op_id].const_bt + # create the pt sub-request + _ = self._inform_pt_sub_request(rq_obj, RQ_SUB_TRIP_ID.FM_PT.value, transfer_street_node, rq_obj.get_destination_node(), fm_est_pt_mod, parent_modal_state, op_id) + + def _process_inform_lastmile_request(self, rid: int, rq_obj: 'BasicIntermodalRequest', sim_time: int, parent_modal_state: RQ_MODAL_STATE = RQ_MODAL_STATE.LASTMILE): + """This method processes the new lastmile request. + + Args: + rid (int): the request id + rq_obj ('BasicIntermodalRequest'): the request object + sim_time (int): the simulation time + parent_modal_state (RQ_MODAL_STATE): the parent modal state + """ + # get the transfer station id and its closest pt station + transfer_station_ids: tp.List[str] = rq_obj.get_transfer_station_ids() + transfer_street_node, _ = self._find_transfer_info(transfer_station_ids[0], "pt2street") + # create sub-request for PT + lm_pt_arrival: tp.Optional[int] = self._inform_pt_sub_request(rq_obj, RQ_SUB_TRIP_ID.LM_PT.value, rq_obj.get_origin_node(), transfer_street_node, rq_obj.earliest_start_time, parent_modal_state) + + if lm_pt_arrival is not None: + # create sub-request for AMoD + for op_id in range(self.n_amod_op): + self._inform_amod_sub_request(rq_obj, RQ_SUB_TRIP_ID.LM_AMOD.value, transfer_street_node, rq_obj.get_destination_node(), lm_pt_arrival, parent_modal_state, op_id, sim_time) + else: + LOG.info(f"PT offer is not available for sub_request {rid}_{RQ_SUB_TRIP_ID.LM_PT.value}, so the lastmile AMoD sub-request will not be created.") + + def _process_inform_firstlastmile_request(self, rid: int, rq_obj: 'BasicIntermodalRequest', sim_time: int, parent_modal_state: RQ_MODAL_STATE = RQ_MODAL_STATE.FIRSTLASTMILE): + """This method processes the new firstlastmile request. + + Args: + rid (int): the request id + rq_obj ('BasicIntermodalRequest'): the request object + sim_time (int): the simulation time + parent_modal_state (RQ_MODAL_STATE): the parent modal state + """ + # get the transfer station ids and their closest pt stations + transfer_station_ids: tp.List[str] = rq_obj.get_transfer_station_ids() + transfer_street_node_0, _ = self._find_transfer_info(transfer_station_ids[0], "pt2street") + transfer_street_node_1, _ = self._find_transfer_info(transfer_station_ids[1], "pt2street") + + # create sub-request for AMoD + for op_id in range(self.n_amod_op): + # firstmile AMoD sub-request + self._inform_amod_sub_request(rq_obj, RQ_SUB_TRIP_ID.FLM_AMOD_0.value, rq_obj.get_origin_node(), transfer_street_node_0, rq_obj.earliest_start_time, parent_modal_state, op_id, sim_time) + flm_amod_rid_struct_0: str = f"{rid}_{RQ_SUB_TRIP_ID.FLM_AMOD_0.value}" + flm_amod_sub_rq_obj_0: BasicIntermodalRequest = self.demand[flm_amod_rid_struct_0] + # create sub-request for PT + # estimate the dropoff time of the amod sub-request + estimated_amod_dropoff_time: int = self._estimate_amod_dropoff_time(op_id, flm_amod_sub_rq_obj_0) + # estimate the earliest start time of the pt sub-request + flm_est_pt_mod: int = estimated_amod_dropoff_time + self.amod_operators[op_id].const_bt + # create the pt sub-request + flm_pt_arrival: tp.Optional[int] = self._inform_pt_sub_request(rq_obj, RQ_SUB_TRIP_ID.FLM_PT.value, transfer_street_node_0,transfer_street_node_1, flm_est_pt_mod,parent_modal_state,op_id) + + # create sub-request for the same AMoD operator + if flm_pt_arrival is None: + raise ValueError(f"PT offer is not available for sub_request {rid}_{RQ_SUB_TRIP_ID.FLM_PT.value}, skipping to the next AMoD operator.") + else: + # last mile AMoD sub-request + self._inform_amod_sub_request(rq_obj, RQ_SUB_TRIP_ID.FLM_AMOD_1.value, transfer_street_node_1, rq_obj.get_destination_node(), flm_pt_arrival, parent_modal_state, op_id, sim_time) + + def _process_collect_firstmile_offers( + self, rid: int, parent_rq_obj: 'BasicIntermodalRequest', parent_modal_state: RQ_MODAL_STATE, offers: tp.Dict[int, 'TravellerOffer'], + ) -> tp.Dict[int, 'TravellerOffer']: + """This method processes the collection of firstmile offers. + """ + # get rid struct for all sections + fm_amod_rid_struct: str = f"{rid}_{RQ_SUB_TRIP_ID.FM_AMOD.value}" + fm_pt_rid_struct: str = f"{rid}_{RQ_SUB_TRIP_ID.FM_PT.value}" + + for amod_op_id in range(self.n_amod_op): + # TODO: if there are multiple AMoD operators, the PT offer can be overwritten here!!! + # 1. collect FM AMoD offer + fm_amod_offer: 'TravellerOffer' = self.amod_operators[amod_op_id].get_current_offer(fm_amod_rid_struct) + LOG.debug(f"Collecting fm_amod offer for request {fm_amod_rid_struct} from operator {amod_op_id}: {fm_amod_offer}.") + # check if FM AMoD offer is available + if fm_amod_offer is None or fm_amod_offer.service_declined(): + LOG.info(f"FM AMoD offer is not available for sub_request {fm_amod_rid_struct}, skipping to next AMoD operator.") + continue + # register the FM AMoD offer in the sub-request + self.demand[fm_amod_rid_struct].receive_offer(amod_op_id, fm_amod_offer, None) + + # 2. collect FM PT offer + fm_pt_offer: 'TravellerOffer' = self.pt_operator.get_current_offer(fm_pt_rid_struct, amod_op_id) + LOG.debug(f"Collecting fm_pt offer for request {fm_pt_rid_struct} from operator {self.pt_operator_id}: {fm_pt_offer}.") + # check if PT offer is available + if fm_pt_offer is None or fm_pt_offer.service_declined(): + LOG.info(f"PT offer is not available for sub_request {fm_pt_rid_struct}, skipping to next AMoD operator.") + continue + # register the PT offer in the sub-request + self.demand[fm_pt_rid_struct].receive_offer(self.pt_operator_id, fm_pt_offer, None) + + # 3. create intermodal offer + sub_trip_offers: tp.Dict[int, TravellerOffer] = {} + sub_trip_offers[RQ_SUB_TRIP_ID.FM_AMOD.value] = fm_amod_offer + sub_trip_offers[RQ_SUB_TRIP_ID.FM_PT.value] = fm_pt_offer + intermodal_offer: 'IntermodalOffer' = self._create_intermodal_offer(rid, sub_trip_offers, parent_modal_state) + LOG.info(f"Created intermodal offer for request {rid}: {intermodal_offer}") + + # for this communication strategy, the FM AMoD DO time does not need to be updated. + + # 4. register the intermodal offer + offers[intermodal_offer.operator_id] = intermodal_offer + return offers + + def _process_collect_lastmile_offers(self, rid: int, parent_modal_state: RQ_MODAL_STATE, offers: tp.Dict[int, 'TravellerOffer']) -> tp.Dict[int, 'TravellerOffer']: + """This method processes the collection of lastmile offers. + """ + # get lastmile pt offer + lm_pt_rid_struct: str = f"{rid}_{RQ_SUB_TRIP_ID.LM_PT.value}" + lm_pt_offer: 'TravellerOffer' = self.pt_operator.get_current_offer(lm_pt_rid_struct) + LOG.debug(f"Collecting lm_pt offer for request {lm_pt_rid_struct} from PT operator {self.pt_operator_id}: {lm_pt_offer}") + + if lm_pt_offer is not None and not lm_pt_offer.service_declined(): + # register the pt offer in the sub-request + self.demand[lm_pt_rid_struct].receive_offer(self.pt_operator_id, lm_pt_offer, None) + lm_amod_rid_struct: str = f"{rid}_{RQ_SUB_TRIP_ID.LM_AMOD.value}" + for amod_op_id in range(self.n_amod_op): + # get lastmile amod offer + lm_amod_offer = self.amod_operators[amod_op_id].get_current_offer(lm_amod_rid_struct) + LOG.debug(f"Collecting lm_amod offer for request {lm_amod_rid_struct} from operator {amod_op_id}: {lm_amod_offer}") + + if lm_amod_offer is not None and not lm_amod_offer.service_declined(): + # register the amod offer in the sub-request + self.demand[lm_amod_rid_struct].receive_offer(amod_op_id, lm_amod_offer, None) + + # create intermodal offer + sub_trip_offers: tp.Dict[int, 'TravellerOffer'] = {} + sub_trip_offers[RQ_SUB_TRIP_ID.LM_PT.value] = lm_pt_offer + sub_trip_offers[RQ_SUB_TRIP_ID.LM_AMOD.value] = lm_amod_offer + intermodal_offer: 'IntermodalOffer' = self._create_intermodal_offer(rid, sub_trip_offers, parent_modal_state) + offers[intermodal_offer.operator_id] = intermodal_offer + else: + LOG.info(f"AMoD offer is not available for sub_request {lm_amod_rid_struct}") + else: + LOG.info(f"PT offer is not available for sub_request {lm_pt_rid_struct}") + return offers + + def _process_collect_firstlastmile_offers( + self, rid: int, parent_rq_obj: 'BasicIntermodalRequest', parent_modal_state: RQ_MODAL_STATE, offers: tp.Dict[int, 'TravellerOffer'], sim_time: int + ) -> tp.Dict[int, 'TravellerOffer']: + """This method processes the collection of firstlastmile offers. + """ + # get rid struct for all sections + flm_amod_rid_struct_0: str = f"{rid}_{RQ_SUB_TRIP_ID.FLM_AMOD_0.value}" + flm_pt_rid_struct: str = f"{rid}_{RQ_SUB_TRIP_ID.FLM_PT.value}" + flm_amod_rid_struct_1: str = f"{rid}_{RQ_SUB_TRIP_ID.FLM_AMOD_1.value}" + + for amod_op_id in range(self.n_amod_op): + # 1. collect FLM AMoD offer 0 + flm_amod_offer_0: 'TravellerOffer' = self.amod_operators[amod_op_id].get_current_offer(flm_amod_rid_struct_0) + LOG.debug(f"Collecting flm_amod_0 offer for request {flm_amod_rid_struct_0} from operator {amod_op_id}: {flm_amod_offer_0}") + # check if FLM AMoD offer 0 is available + if flm_amod_offer_0 is None or flm_amod_offer_0.service_declined(): + LOG.info(f"AMoD offer is not available for sub_request {flm_amod_rid_struct_0}, skipping to next AMoD operator.") + continue + self.demand[flm_amod_rid_struct_0].receive_offer(amod_op_id, flm_amod_offer_0, None) + + # 2. collect FLM PT offer + flm_pt_offer: 'TravellerOffer' = self.pt_operator.get_current_offer(flm_pt_rid_struct, amod_op_id) + LOG.debug(f"Collecting flm_pt offer for request {flm_pt_rid_struct} from operator {self.pt_operator_id}: {flm_pt_offer}") + # check if PT offer is available + if flm_pt_offer is None or flm_pt_offer.service_declined(): + LOG.info(f"PT offer is not available for sub_request {flm_pt_rid_struct}, skipping to next AMoD operator.") + continue + self.demand[flm_pt_rid_struct].receive_offer(self.pt_operator_id, flm_pt_offer, None) + + # 3. collect FLM AMoD offer 1 + flm_amod_offer_1: 'TravellerOffer' = self.amod_operators[amod_op_id].get_current_offer(flm_amod_rid_struct_1) + LOG.debug(f"Collecting flm_amod_1 offer for request {flm_amod_rid_struct_1} from operator {amod_op_id}: {flm_amod_offer_1}") + # check if FLM AMoD offer 1 is available + if flm_amod_offer_1 is None or flm_amod_offer_1.service_declined(): + LOG.info(f"AMoD offer is not available for sub_request {flm_amod_rid_struct_1}, skipping to next AMoD operator.") + continue + self.demand[flm_amod_rid_struct_1].receive_offer(amod_op_id, flm_amod_offer_1, None) + + # 4. create intermodal offer + sub_trip_offers: tp.Dict[int, 'TravellerOffer'] = {} + sub_trip_offers[RQ_SUB_TRIP_ID.FLM_AMOD_0.value] = flm_amod_offer_0 + sub_trip_offers[RQ_SUB_TRIP_ID.FLM_PT.value] = flm_pt_offer + sub_trip_offers[RQ_SUB_TRIP_ID.FLM_AMOD_1.value] = flm_amod_offer_1 + intermodal_offer: 'IntermodalOffer' = self._create_intermodal_offer(rid, sub_trip_offers, parent_modal_state) + LOG.info(f"Created intermodal offer for request {rid}: {intermodal_offer}") + + # for this communication strategy, the FLM AMoD DO time does not need to be updated. + + # 5. register the intermodal offer + offers[intermodal_offer.operator_id] = intermodal_offer + return offers + + def _estimate_amod_dropoff_time(self, amod_op_id: int, sub_rq_obj: 'BasicIntermodalRequest') -> tp.Optional[int]: + """This method estimates the dropoff time of an amod sub-request. + This time point marks the start of alighting the FM amod vehicle. + + Args: + amod_op_id (int): the id of the amod operator + sub_rq_obj (BasicIntermodalRequest): the sub-request object + Returns: + int: the dropoff time of the sub-request + """ + sub_rq_rid_struct: str = sub_rq_obj.get_rid_struct() + sub_prq_obj: PlanRequest = self.amod_operators[amod_op_id].rq_dict.get(sub_rq_rid_struct, None) + + prq_direct_tt = sub_prq_obj.init_direct_tt + amod_boarding_time = self.amod_operators[amod_op_id].const_bt + amod_max_dtf = self.amod_operators[amod_op_id].max_dtf / 100 + prq_pu_latest = sub_prq_obj.t_pu_latest + + # MaaS estimates that only a certain percentage of the operator maximum detour time will be realized + maas_estimated_prq_max_trip_time = (1 + self.maas_detour_time_factor * amod_max_dtf) * (prq_direct_tt + amod_boarding_time) + + maas_estimated_latest_dropoff_time: int = prq_pu_latest + int(maas_estimated_prq_max_trip_time) + return maas_estimated_latest_dropoff_time + + def acknowledge_user_alighting(self, op_id: int, rid_struct: str, vid: int, alighting_time: int): + """Override to check if FM passenger can catch their PT connection. + + After FM AMoD alighting completes, check if the alighting time is still + within the PT offer's origin_node_latest_arrival_time. If not, cancel + subsequent offers and mark the request as uncatchable. + + Args: + op_id (int): the AMoD operator id + rid_struct (str): the request id struct (e.g., "123_1" for sub-request) + vid (int): the vehicle id + alighting_time (int): the simulation time when alighting completes + """ + # Call parent implementation first + super().acknowledge_user_alighting(op_id, rid_struct, vid, alighting_time) + + # Check if this is a FM or FLM first-leg AMoD sub-request + rid_struct_str = str(rid_struct) + if "_" in rid_struct_str: + parts = rid_struct_str.rsplit("_", 1) + parent_rid = int(parts[0]) + sub_trip_id = int(parts[1]) + + # Check FM case: FM_AMOD alighting completed + if sub_trip_id == RQ_SUB_TRIP_ID.FM_AMOD.value: + self._check_fm_pt_catchability(parent_rid, sub_trip_id, alighting_time, RQ_SUB_TRIP_ID.FM_PT.value, op_id) + # Check FLM case: FLM_AMOD_0 alighting completed + elif sub_trip_id == RQ_SUB_TRIP_ID.FLM_AMOD_0.value: + self._check_fm_pt_catchability(parent_rid, sub_trip_id, alighting_time, RQ_SUB_TRIP_ID.FLM_PT.value, op_id) + + def _check_fm_pt_catchability(self, parent_rid: int, amod_sub_trip_id: int, alighting_time: int, pt_sub_trip_id: int, amod_op_id: int): + """Check if passenger can catch their PT connection after FM AMoD alighting. + + Args: + parent_rid (int): the parent request id + amod_sub_trip_id (int): the sub-trip id of the completed AMoD leg + alighting_time (int): the simulation time when alighting completes + pt_sub_trip_id (int): the sub-trip id of the PT leg to check + amod_op_id (int): the AMoD operator id that served the FM leg + """ + # Get PT offer + pt_rid_struct = f"{parent_rid}_{pt_sub_trip_id}" + + # Get the PT offer for the specific AMoD operator that served the FM leg + pt_offer: 'PTOffer' = self.pt_operator.get_current_offer(pt_rid_struct, amod_op_id) + + if pt_offer is None or pt_offer.service_declined(): + LOG.debug(f"No PT offer found for {pt_rid_struct}, skipping catchability check") + return + + # Check catchability: alighting_time vs origin_node_latest_arrival_time + origin_node_latest_arrival_time = pt_offer.origin_node_latest_arrival_time + + if alighting_time > origin_node_latest_arrival_time: + LOG.warning(f"Request {parent_rid}: PT uncatchable! " + f"Alighting time {alighting_time} > PT latest arrival {origin_node_latest_arrival_time} " + f"(delay: {alighting_time - origin_node_latest_arrival_time}s)") + self._handle_uncatchable_pt(parent_rid, alighting_time, amod_op_id) + else: + LOG.debug(f"Request {parent_rid}: PT catchable. " + f"Alighting time {alighting_time} <= PT latest arrival {origin_node_latest_arrival_time} " + f"(buffer: {origin_node_latest_arrival_time - alighting_time}s)") + + def _handle_uncatchable_pt(self, parent_rid: int, sim_time: int, amod_op_id: int): + """Handle the case when passenger cannot catch their PT connection. + + This method: + 1. Marks the parent request as uncatchable + 2. Cancels subsequent sub-requests (PT and any LM AMoD) + + Args: + parent_rid (int): the parent request id + sim_time (int): the current simulation time + amod_op_id (int): the AMoD operator id that served the FM leg + """ + parent_rq_obj: 'BasicIntermodalRequest' = self.demand[parent_rid] + parent_modal_state: RQ_MODAL_STATE = parent_rq_obj.get_modal_state() + + # Mark the request as uncatchable + parent_rq_obj.set_uncatchable_pt(True) + LOG.info(f"Request {parent_rid} marked as uncatchable_pt") + + # Cancel subsequent sub-requests based on modal state + if parent_modal_state == RQ_MODAL_STATE.FIRSTMILE: + # For FM: cancel PT sub-request + pt_rid_struct = f"{parent_rid}_{RQ_SUB_TRIP_ID.FM_PT.value}" + try: + self.pt_operator.user_cancels_request(pt_rid_struct, sim_time, amod_op_id) + LOG.info(f"Cancelled PT sub-request {pt_rid_struct} due to uncatchable PT") + except (KeyError, AttributeError) as e: + LOG.debug(f"Could not cancel PT sub-request {pt_rid_struct}: {e}") + + elif parent_modal_state == RQ_MODAL_STATE.FIRSTLASTMILE: + # For FLM: cancel PT and last-mile AMoD sub-requests + pt_rid_struct = f"{parent_rid}_{RQ_SUB_TRIP_ID.FLM_PT.value}" + lm_amod_rid_struct = f"{parent_rid}_{RQ_SUB_TRIP_ID.FLM_AMOD_1.value}" + + # Cancel PT sub-request + try: + self.pt_operator.user_cancels_request(pt_rid_struct, sim_time, amod_op_id) + LOG.info(f"Cancelled PT sub-request {pt_rid_struct} due to uncatchable PT") + except (KeyError, AttributeError) as e: + LOG.debug(f"Could not cancel PT sub-request {pt_rid_struct}: {e}") + + # Cancel last-mile AMoD sub-request + for op in self.amod_operators: + try: + op.user_cancels_request(lm_amod_rid_struct, sim_time) + LOG.info(f"Cancelled LM AMoD sub-request {lm_amod_rid_struct} due to uncatchable PT") + except KeyError: + LOG.debug(f"LM AMoD sub-request {lm_amod_rid_struct} not found for operator, may not exist") \ No newline at end of file diff --git a/src/broker/PTBrokerPAYG.py b/src/broker/PTBrokerPAYG.py new file mode 100644 index 00000000..92a2dabc --- /dev/null +++ b/src/broker/PTBrokerPAYG.py @@ -0,0 +1,531 @@ +# -------------------------------------------------------------------------------------------------------------------- # +# PTBrokerPAYG: Plan-As-You-Go (PAYG) Broker Strategy +# +# This broker simulates traveler behavior without a MaaS platform — travelers plan their trip step by step, +# querying the next leg only after completing the current one: +# - FIRSTMILE (FM): At request time, create only FM_AMOD (origin → PT boarding stop). +# After AMoD alighting, query PT in real-time and create the PT sub-request. +# - LASTMILE (LM): At request time, query PT (origin → PT alighting stop). +# After PT alighting, request LM_AMOD in real-time (PT alighting stop → destination). +# - FIRSTLASTMILE (FLM): At request time, create only FLM_AMOD_0 (origin → PT boarding stop). +# After AMoD alighting, query PT in real-time. After PT alighting, request FLM_AMOD_1 in real-time. +# If any step fails to find an available service, the trip is marked as interrupted. +# Unlike PTBrokerBasic (TPCS), no pre-planned combined offer is presented to the user upfront. +# +# NOTE: This code has only been tested and applied in the ImmediateDecisionsSimulation environment +# combined with the PoolingIRSOnly fleet controller. +# -------------------------------------------------------------------------------------------------------------------- # + +# standard distribution imports +import logging +import typing as tp + +# src imports +from src.broker.PTBrokerBasic import PTBrokerBasic +from src.simulation.Offers import TravellerOffer + +if tp.TYPE_CHECKING: + from src.fleetctrl.FleetControlBase import FleetControlBase + from src.ptctrl.PTControlBase import PTControlBase + from src.demand.demand import Demand + from src.routing.road.NetworkBase import NetworkBase + from src.demand.TravelerModels import RequestBase, BasicIntermodalRequest + from src.simulation.Offers import PTOffer + +# -------------------------------------------------------------------------------------------------------------------- # +# global variables +from src.misc.globals import * + +LOG = logging.getLogger(__name__) + + +INPUT_PARAMETERS_PTBrokerPAYG = { + "doc": "Plan-As-You-Go broker: simulates travelers planning trips step by step without MaaS platform integration", + "inherit": PTBrokerBasic, + "input_parameters_mandatory": ["n_amod_op", "amod_operators", "pt_operator", "demand", "routing_engine", "scenario_parameters"], + "input_parameters_optional": [], + "mandatory_modules": [], + "optional_modules": [] +} + + +class PTBrokerPAYG(PTBrokerBasic): + """ + Plan-As-You-Go (PAYG) broker strategy. + + Unlike typical PTBroker which plans the entire intermodal trip upfront, PAYG simulates + travelers who plan only the next step of their journey: + + - FM/FLM requests: Only create FM_AMOD at request time; PT is queried after AMoD alighting + - LM requests: Query PT at request time; LM_AMOD is requested after PT alighting + - FLM requests: FM_AMOD -> (alighting) -> PT query -> (PT arrival) -> LM_AMOD request + + If any step fails to find an available service, the trip is marked as interrupted. + """ + + def __init__( + self, + n_amod_op: int, + amod_operators: tp.List['FleetControlBase'], + pt_operator: 'PTControlBase', + demand: 'Demand', + routing_engine: 'NetworkBase', + scenario_parameters: dict, + ): + super().__init__(n_amod_op, amod_operators, pt_operator, demand, routing_engine, scenario_parameters) + + # PAYG-specific state tracking + self.payg_trip_states: tp.Dict[int, PAYG_TRIP_STATE] = {} # rid -> state + + # Pending PT arrivals: rid -> (pt_arrival_time, pt_alighting_node) + # Used to trigger LM_AMOD requests when PT arrives + self.pending_pt_arrivals: tp.Dict[int, tp.Tuple[int, int]] = {} + + # ============================================================================================================== # + # Request Processing Methods + # ============================================================================================================== # + + def _process_inform_firstmile_request( + self, rid: int, rq_obj: 'BasicIntermodalRequest', sim_time: int, + parent_modal_state: RQ_MODAL_STATE = RQ_MODAL_STATE.FIRSTMILE + ): + """Process FM request: Only create FM_AMOD sub-request. + PT will be queried after FM_AMOD alighting. + """ + # Get transfer station + transfer_station_ids: tp.List[str] = rq_obj.get_transfer_station_ids() + transfer_street_node, _ = self._find_transfer_info(transfer_station_ids[0], "pt2street") + + # Create FM_AMOD sub-request for each operator + for op_id in range(self.n_amod_op): + self._inform_amod_sub_request( + rq_obj, RQ_SUB_TRIP_ID.FM_AMOD.value, + rq_obj.get_origin_node(), transfer_street_node, + rq_obj.earliest_start_time, parent_modal_state, op_id, sim_time + ) + + # Initialize PAYG state + self.payg_trip_states[rid] = PAYG_TRIP_STATE.PENDING + LOG.debug(f"PAYG FM request {rid}: Created FM_AMOD sub-request, PT will be queried after alighting") + + def _process_inform_lastmile_request( + self, rid: int, rq_obj: 'BasicIntermodalRequest', sim_time: int, + parent_modal_state: RQ_MODAL_STATE = RQ_MODAL_STATE.LASTMILE + ): + """Process LM request: Query PT first. LM_AMOD will be scheduled after user confirms booking. + """ + # Get transfer station + transfer_station_ids: tp.List[str] = rq_obj.get_transfer_station_ids() + transfer_street_node, _ = self._find_transfer_info(transfer_station_ids[0], "pt2street") + + # Query PT immediately + lm_pt_arrival: tp.Optional[int] = self._inform_pt_sub_request( + rq_obj, RQ_SUB_TRIP_ID.LM_PT.value, + rq_obj.get_origin_node(), transfer_street_node, + rq_obj.earliest_start_time, parent_modal_state + ) + + if lm_pt_arrival is not None: + # PT offer available - state will be updated when user confirms booking + self.payg_trip_states[rid] = PAYG_TRIP_STATE.PENDING + LOG.debug(f"PAYG LM request {rid}: PT offer available, waiting for user booking confirmation") + else: + # No PT available - mark as interrupted + self._mark_trip_interrupted(rid, PAYG_TRIP_STATE.INTERRUPTED_NO_PT, sim_time) + LOG.info(f"PAYG LM request {rid}: No PT available, trip interrupted") + + def _process_inform_firstlastmile_request( + self, rid: int, rq_obj: 'BasicIntermodalRequest', sim_time: int, + parent_modal_state: RQ_MODAL_STATE = RQ_MODAL_STATE.FIRSTLASTMILE + ): + """Process FLM request: Only create FLM_AMOD_0 sub-request. + PT and LM_AMOD will be created after respective alighting events. + """ + # Get first transfer station + transfer_station_ids: tp.List[str] = rq_obj.get_transfer_station_ids() + transfer_street_node_0, _ = self._find_transfer_info(transfer_station_ids[0], "pt2street") + + # Create FLM_AMOD_0 sub-request for each operator + for op_id in range(self.n_amod_op): + self._inform_amod_sub_request( + rq_obj, RQ_SUB_TRIP_ID.FLM_AMOD_0.value, + rq_obj.get_origin_node(), transfer_street_node_0, + rq_obj.earliest_start_time, parent_modal_state, op_id, sim_time + ) + + # Initialize PAYG state + self.payg_trip_states[rid] = PAYG_TRIP_STATE.PENDING + LOG.debug(f"PAYG FLM request {rid}: Created FLM_AMOD_0 sub-request, PT will be queried after alighting") + + # ============================================================================================================== # + # Offer Collection Methods + # ============================================================================================================== # + + def collect_offers(self, rid: int, sim_time: int) -> tp.Dict[int, 'TravellerOffer']: + """Collect offers, processing any pending PT arrivals first.""" + # Process pending PT arrivals before collecting offers + self._process_pending_pt_arrivals(sim_time) + + # Call parent implementation + return super().collect_offers(rid, sim_time) + + def _process_collect_firstmile_offers( + self, rid: int, parent_rq_obj: 'BasicIntermodalRequest', parent_modal_state: RQ_MODAL_STATE, + offers: tp.Dict[int, 'TravellerOffer'] + ) -> tp.Dict[int, 'TravellerOffer']: + """Collect FM offers: Only return FM_AMOD offers (PT is not yet queried in PAYG mode).""" + fm_amod_rid_struct: str = f"{rid}_{RQ_SUB_TRIP_ID.FM_AMOD.value}" + + for amod_op_id in range(self.n_amod_op): + fm_amod_offer: 'TravellerOffer' = self.amod_operators[amod_op_id].get_current_offer(fm_amod_rid_struct) + LOG.debug(f"PAYG collecting FM_AMOD offer for {fm_amod_rid_struct} from operator {amod_op_id}: {fm_amod_offer}") + + if fm_amod_offer is not None and not fm_amod_offer.service_declined(): + # Register offer + self.demand[fm_amod_rid_struct].receive_offer(amod_op_id, fm_amod_offer, None) + # Return single-leg offer (not IntermodalOffer since PT is unknown) + offers[amod_op_id] = fm_amod_offer + + return offers + + def _process_collect_lastmile_offers( + self, rid: int, parent_modal_state: RQ_MODAL_STATE, + offers: tp.Dict[int, 'TravellerOffer'] + ) -> tp.Dict[int, 'TravellerOffer']: + """Collect LM offers: Return PT offer only (LM_AMOD not yet requested in PAYG mode).""" + lm_pt_rid_struct: str = f"{rid}_{RQ_SUB_TRIP_ID.LM_PT.value}" + lm_pt_offer: 'TravellerOffer' = self.pt_operator.get_current_offer(lm_pt_rid_struct) + LOG.debug(f"PAYG collecting LM_PT offer for {lm_pt_rid_struct}: {lm_pt_offer}") + + if lm_pt_offer is not None and not lm_pt_offer.service_declined(): + self.demand[lm_pt_rid_struct].receive_offer(self.pt_operator_id, lm_pt_offer, None) + # Return PT offer only + offers[self.pt_operator_id] = lm_pt_offer + + return offers + + def _process_collect_firstlastmile_offers( + self, rid: int, parent_rq_obj: 'BasicIntermodalRequest', parent_modal_state: RQ_MODAL_STATE, + offers: tp.Dict[int, 'TravellerOffer'], sim_time: int + ) -> tp.Dict[int, 'TravellerOffer']: + """Collect FLM offers: Only return FLM_AMOD_0 offers (PT and LM not yet queried in PAYG mode).""" + flm_amod_rid_struct_0: str = f"{rid}_{RQ_SUB_TRIP_ID.FLM_AMOD_0.value}" + + for amod_op_id in range(self.n_amod_op): + flm_amod_offer_0: 'TravellerOffer' = self.amod_operators[amod_op_id].get_current_offer(flm_amod_rid_struct_0) + LOG.debug(f"PAYG collecting FLM_AMOD_0 offer for {flm_amod_rid_struct_0} from operator {amod_op_id}: {flm_amod_offer_0}") + + if flm_amod_offer_0 is not None and not flm_amod_offer_0.service_declined(): + self.demand[flm_amod_rid_struct_0].receive_offer(amod_op_id, flm_amod_offer_0, None) + # Return single-leg offer + offers[amod_op_id] = flm_amod_offer_0 + + return offers + + # ============================================================================================================== # + # Booking Methods + # ============================================================================================================== # + + def inform_user_booking(self, rid: int, rq_obj: 'RequestBase', sim_time: int, chosen_operator: tp.Union[int, tuple]) -> tp.List[tuple[int, 'RequestBase']]: + """Handle user booking for PAYG mode.""" + amod_confirmed_rids = [] + parent_modal_state: RQ_MODAL_STATE = rq_obj.get_modal_state() + + # Check if this is a pure PT offer selection (not LM PT offer in PAYG mode) + # For LM requests in PAYG, chosen_operator == pt_operator_id means LM_PT offer, not pure PT + if chosen_operator == self.pt_operator_id and parent_modal_state != RQ_MODAL_STATE.LASTMILE: + amod_confirmed_rids.append((rid, rq_obj)) + # inform all AMoD operators that the request is cancelled + self.inform_user_leaving_system(rid, sim_time) + pt_rid_struct: str = f"{rid}_{RQ_SUB_TRIP_ID.PT.value}" + pt_sub_rq_obj = self.demand[pt_rid_struct] + self.pt_operator.user_confirms_booking(pt_sub_rq_obj, None) + return amod_confirmed_rids + + # PAYG mode: Handle single-leg bookings for FM/FLM + if parent_modal_state == RQ_MODAL_STATE.MONOMODAL or parent_modal_state == RQ_MODAL_STATE.PT: + # Standard monomodal handling + for i, operator in enumerate(self.amod_operators): + if i != chosen_operator: + operator.user_cancels_request(rid, sim_time) + else: + operator.user_confirms_booking(rid, sim_time) + amod_confirmed_rids.append((rid, rq_obj)) + + elif parent_modal_state == RQ_MODAL_STATE.FIRSTMILE: + # PAYG FM: Book only FM_AMOD, PT will be queried later + fm_amod_rid_struct = f"{rid}_{RQ_SUB_TRIP_ID.FM_AMOD.value}" + for i, operator in enumerate(self.amod_operators): + if i != chosen_operator: + operator.user_cancels_request(fm_amod_rid_struct, sim_time) + else: + operator.user_confirms_booking(fm_amod_rid_struct, sim_time) + self.payg_trip_states[rid] = PAYG_TRIP_STATE.FM_AMOD_BOOKED + amod_confirmed_rids.append((rid, rq_obj)) + LOG.debug(f"PAYG FM booking {rid}: FM_AMOD booked with operator {chosen_operator}") + + elif parent_modal_state == RQ_MODAL_STATE.LASTMILE: + # PAYG LM: Book PT, schedule LM_AMOD request for after PT arrival + lm_pt_rid_struct = f"{rid}_{RQ_SUB_TRIP_ID.LM_PT.value}" + lm_pt_sub_rq_obj = self.demand[lm_pt_rid_struct] + self.pt_operator.user_confirms_booking(lm_pt_sub_rq_obj, None) + + # Get PT offer to retrieve arrival time and transfer node + lm_pt_offer: 'PTOffer' = self.pt_operator.get_current_offer(lm_pt_rid_struct) + if lm_pt_offer is not None and not lm_pt_offer.service_declined(): + pt_arrival_time = lm_pt_offer.destination_node_arrival_time + # Get transfer street node from request + transfer_station_ids = rq_obj.get_transfer_station_ids() + transfer_street_node, _ = self._find_transfer_info(transfer_station_ids[0], "pt2street") + # Schedule LM_AMOD request for PT arrival + self.pending_pt_arrivals[rid] = (pt_arrival_time, transfer_street_node) + LOG.debug(f"PAYG LM booking {rid}: PT booked, LM_AMOD scheduled at {pt_arrival_time}") + + self.payg_trip_states[rid] = PAYG_TRIP_STATE.PT_BOOKED + amod_confirmed_rids.append((rid, rq_obj)) + LOG.debug(f"PAYG LM booking {rid}: PT booked") + + elif parent_modal_state == RQ_MODAL_STATE.FIRSTLASTMILE: + # PAYG FLM: Book only FLM_AMOD_0, PT and LM will be handled later + flm_amod_rid_struct_0 = f"{rid}_{RQ_SUB_TRIP_ID.FLM_AMOD_0.value}" + for i, operator in enumerate(self.amod_operators): + if i != chosen_operator: + operator.user_cancels_request(flm_amod_rid_struct_0, sim_time) + else: + operator.user_confirms_booking(flm_amod_rid_struct_0, sim_time) + self.payg_trip_states[rid] = PAYG_TRIP_STATE.FM_AMOD_BOOKED + amod_confirmed_rids.append((rid, rq_obj)) + LOG.debug(f"PAYG FLM booking {rid}: FLM_AMOD_0 booked with operator {chosen_operator}") + + else: + raise ValueError(f"Invalid modal state: {parent_modal_state}") + + return amod_confirmed_rids + + # ============================================================================================================== # + # Alighting Event Handlers + # ============================================================================================================== # + + def acknowledge_user_alighting(self, op_id: int, rid_struct: str, vid: int, alighting_time: int): + """Handle AMoD alighting events - trigger next step in PAYG flow.""" + # Call parent implementation first + super().acknowledge_user_alighting(op_id, rid_struct, vid, alighting_time) + + # Parse rid_struct + rid_struct_str = str(rid_struct) + if "_" not in rid_struct_str: + return # Not a sub-request + + parts = rid_struct_str.rsplit("_", 1) + parent_rid = int(parts[0]) + sub_trip_id = int(parts[1]) + + parent_rq_obj: 'BasicIntermodalRequest' = self.demand[parent_rid] + parent_modal_state: RQ_MODAL_STATE = parent_rq_obj.get_modal_state() + + # Get alighting node from parent request's transfer station info + # (sub-request is already deleted from rq_db by user_ends_alighting) + transfer_station_ids: tp.List[str] = parent_rq_obj.get_transfer_station_ids() + + # FM_AMOD alighting -> Query PT + if sub_trip_id == RQ_SUB_TRIP_ID.FM_AMOD.value: + # FM_AMOD destination is the first transfer station + alighting_node, _ = self._find_transfer_info(transfer_station_ids[0], "pt2street") + self._handle_fm_amod_alighting(parent_rid, parent_rq_obj, op_id, alighting_time, parent_modal_state, alighting_node) + + # FLM_AMOD_0 alighting -> Query PT + elif sub_trip_id == RQ_SUB_TRIP_ID.FLM_AMOD_0.value: + # FLM_AMOD_0 destination is the first transfer station + alighting_node, _ = self._find_transfer_info(transfer_station_ids[0], "pt2street") + self._handle_flm_amod_0_alighting(parent_rid, parent_rq_obj, op_id, alighting_time, parent_modal_state, alighting_node) + + # LM_AMOD alighting -> Trip completed + elif sub_trip_id == RQ_SUB_TRIP_ID.LM_AMOD.value: + self._handle_trip_completed(parent_rid, alighting_time) + + # FLM_AMOD_1 alighting -> Trip completed + elif sub_trip_id == RQ_SUB_TRIP_ID.FLM_AMOD_1.value: + self._handle_trip_completed(parent_rid, alighting_time) + + def _handle_fm_amod_alighting( + self, rid: int, rq_obj: 'BasicIntermodalRequest', amod_op_id: int, + alighting_time: int, parent_modal_state: RQ_MODAL_STATE, alighting_node: int + ): + """Handle FM_AMOD alighting: Query PT in real-time.""" + LOG.debug(f"PAYG FM alighting: rid={rid}, time={alighting_time}, node={alighting_node}") + + # Query PT from current location/time to destination + pt_arrival = self._inform_pt_sub_request( + rq_obj, RQ_SUB_TRIP_ID.FM_PT.value, + alighting_node, rq_obj.get_destination_node(), + alighting_time, parent_modal_state, amod_op_id + ) + + if pt_arrival is None: + # No PT available + self._mark_trip_interrupted(rid, PAYG_TRIP_STATE.INTERRUPTED_NO_PT, alighting_time) + LOG.info(f"PAYG FM {rid}: No PT available after alighting, trip interrupted") + return + + # Get PT offer and record it on sub-request + pt_rid_struct = f"{rid}_{RQ_SUB_TRIP_ID.FM_PT.value}" + pt_sub_rq_obj = self.demand[pt_rid_struct] + # Clear inherited AMOD offers from deepcopy of parent request + pt_sub_rq_obj.offer = {} + pt_offer = self.pt_operator.get_current_offer(pt_rid_struct, amod_op_id) + if pt_offer is not None: + pt_sub_rq_obj.receive_offer(self.pt_operator_id, pt_offer, None) + + # Auto-confirm PT booking (user is already at the station) + self.pt_operator.user_confirms_booking(pt_sub_rq_obj, amod_op_id) + + self.payg_trip_states[rid] = PAYG_TRIP_STATE.PT_BOOKED + LOG.info(f"PAYG FM {rid}: PT booked after alighting, arrival at {pt_arrival}") + + def _handle_flm_amod_0_alighting( + self, rid: int, rq_obj: 'BasicIntermodalRequest', amod_op_id: int, + alighting_time: int, parent_modal_state: RQ_MODAL_STATE, alighting_node: int + ): + """Handle FLM_AMOD_0 alighting: Query PT in real-time.""" + LOG.debug(f"PAYG FLM_AMOD_0 alighting: rid={rid}, time={alighting_time}, node={alighting_node}") + + # Get transfer stations + transfer_station_ids: tp.List[str] = rq_obj.get_transfer_station_ids() + + # Get second transfer station for PT destination + transfer_street_node_1, _ = self._find_transfer_info(transfer_station_ids[1], "pt2street") + + # Query PT from current location/time + pt_arrival = self._inform_pt_sub_request( + rq_obj, RQ_SUB_TRIP_ID.FLM_PT.value, + alighting_node, transfer_street_node_1, + alighting_time, parent_modal_state, amod_op_id + ) + + if pt_arrival is None: + # No PT available + self._mark_trip_interrupted(rid, PAYG_TRIP_STATE.INTERRUPTED_NO_PT, alighting_time) + LOG.info(f"PAYG FLM {rid}: No PT available after FM alighting, trip interrupted") + return + + # Get PT offer and record it on sub-request + pt_rid_struct = f"{rid}_{RQ_SUB_TRIP_ID.FLM_PT.value}" + pt_sub_rq_obj = self.demand[pt_rid_struct] + # Clear inherited AMOD offers from deepcopy of parent request + pt_sub_rq_obj.offer = {} + pt_offer = self.pt_operator.get_current_offer(pt_rid_struct, amod_op_id) + if pt_offer is not None: + pt_sub_rq_obj.receive_offer(self.pt_operator_id, pt_offer, None) + + # Auto-confirm PT booking + self.pt_operator.user_confirms_booking(pt_sub_rq_obj, amod_op_id) + + # Schedule LM_AMOD request for PT arrival + self.pending_pt_arrivals[rid] = (pt_arrival, transfer_street_node_1) + self.payg_trip_states[rid] = PAYG_TRIP_STATE.PT_BOOKED + LOG.info(f"PAYG FLM {rid}: PT booked, LM_AMOD will be requested at {pt_arrival}") + + # ============================================================================================================== # + # PT Arrival Processing + # ============================================================================================================== # + + def _process_pending_pt_arrivals(self, sim_time: int): + """Process pending PT arrivals and trigger LM_AMOD requests.""" + completed_arrivals = [] + + for rid, (pt_arrival_time, alighting_node) in self.pending_pt_arrivals.items(): + if sim_time >= pt_arrival_time: + # PT has arrived, request LM_AMOD + self._handle_pt_alighting(rid, pt_arrival_time, alighting_node) + completed_arrivals.append(rid) + + # Remove processed arrivals + for rid in completed_arrivals: + del self.pending_pt_arrivals[rid] + + def _handle_pt_alighting(self, rid: int, alighting_time: int, alighting_node: int): + """Handle PT alighting: Request LM_AMOD in real-time.""" + LOG.debug(f"PAYG PT alighting: rid={rid}, time={alighting_time}") + + parent_rq_obj: 'BasicIntermodalRequest' = self.demand[rid] + parent_modal_state: RQ_MODAL_STATE = parent_rq_obj.get_modal_state() + + # Determine sub-trip ID based on modal state + if parent_modal_state == RQ_MODAL_STATE.LASTMILE: + lm_sub_trip_id = RQ_SUB_TRIP_ID.LM_AMOD.value + elif parent_modal_state == RQ_MODAL_STATE.FIRSTLASTMILE: + lm_sub_trip_id = RQ_SUB_TRIP_ID.FLM_AMOD_1.value + else: + LOG.warning(f"PAYG PT alighting for unexpected modal state: {parent_modal_state}") + return + + # Create LM_AMOD sub-request + for op_id in range(self.n_amod_op): + self._inform_amod_sub_request( + parent_rq_obj, lm_sub_trip_id, + alighting_node, parent_rq_obj.get_destination_node(), + alighting_time, parent_modal_state, op_id, alighting_time + ) + + # Get offers and select best one + # TODO: Could implement user choice here instead of auto-selecting best offer + lm_amod_rid_struct = f"{rid}_{lm_sub_trip_id}" + best_offer = None + best_op_id = None + + for amod_op_id in range(self.n_amod_op): + offer = self.amod_operators[amod_op_id].get_current_offer(lm_amod_rid_struct) + if offer is not None and not offer.service_declined(): + if best_offer is None or offer.offered_waiting_time < best_offer.offered_waiting_time: + best_offer = offer + best_op_id = amod_op_id + + if best_offer is None: + # No AMoD available + self._mark_trip_interrupted(rid, PAYG_TRIP_STATE.INTERRUPTED_NO_LM_AMOD, alighting_time) + LOG.info(f"PAYG {rid}: No LM_AMOD available after PT alighting, trip interrupted") + return + + # Auto-confirm best LM_AMOD offer + for op_id in range(self.n_amod_op): + if op_id != best_op_id: + self.amod_operators[op_id].user_cancels_request(lm_amod_rid_struct, alighting_time) + else: + self.amod_operators[op_id].user_confirms_booking(lm_amod_rid_struct, alighting_time) + self.demand[lm_amod_rid_struct].receive_offer(op_id, best_offer, None) + + self.payg_trip_states[rid] = PAYG_TRIP_STATE.LM_AMOD_BOOKED + LOG.info(f"PAYG {rid}: LM_AMOD booked with operator {best_op_id}, wait time {best_offer.offered_waiting_time}s") + + # ============================================================================================================== # + # Trip State Management + # ============================================================================================================== # + + def _handle_trip_completed(self, rid: int, completion_time: int): + """Mark trip as completed.""" + self.payg_trip_states[rid] = PAYG_TRIP_STATE.COMPLETED + LOG.info(f"PAYG trip {rid} completed at {completion_time}") + + def _mark_trip_interrupted(self, rid: int, interrupt_state: PAYG_TRIP_STATE, interrupt_time: int): + """Mark trip as interrupted and record the state.""" + self.payg_trip_states[rid] = interrupt_state + + # Set interrupted flag on parent request + parent_rq_obj: 'BasicIntermodalRequest' = self.demand[rid] + if hasattr(parent_rq_obj, 'set_payg_interrupted'): + parent_rq_obj.set_payg_interrupted(True, interrupt_state.value, interrupt_time) + + LOG.warning(f"PAYG trip {rid} interrupted: {interrupt_state.name} at time {interrupt_time}") + + # ============================================================================================================== # + # User Leaving System + # ============================================================================================================== # + + def inform_user_leaving_system(self, rid: int, sim_time: int): + """Handle user leaving system - cancel any pending requests.""" + # Remove from pending PT arrivals if present + if rid in self.pending_pt_arrivals: + del self.pending_pt_arrivals[rid] + + # Call parent implementation + super().inform_user_leaving_system(rid, sim_time) diff --git a/src/demand/TravelerModels.py b/src/demand/TravelerModels.py index f55f913a..2a3efa77 100644 --- a/src/demand/TravelerModels.py +++ b/src/demand/TravelerModels.py @@ -5,6 +5,7 @@ import os from copy import deepcopy from abc import abstractmethod, ABCMeta +import typing as tp # additional module imports (> requirements) # ------------------------------------------ @@ -15,7 +16,10 @@ # src imports # ----------- from src.misc.functions import PiecewiseContinuousLinearFunction -from src.routing.NetworkBase import return_position_str +from src.routing.road.NetworkBase import return_position_str +if tp.TYPE_CHECKING: + from src.routing.road.NetworkBase import NetworkBase + # -------------------------------------------------------------------------------------------------------------------- # # global variables # ---------------- @@ -52,6 +56,7 @@ class RequestBase(metaclass=ABCMeta): def __init__(self, rq_row, routing_engine, simulation_time_step, scenario_parameters): # input self.rid = int(rq_row.get(G_RQ_ID, rq_row.name)) # request id is index of dataframe + self.subtrip_id: int = None self.sub_rid_struct = None self.is_parcel = False # requests are usually persons self.rq_time = rq_row[G_RQ_TIME] - rq_row[G_RQ_TIME] % simulation_time_step @@ -90,7 +95,7 @@ def __init__(self, rq_row, routing_engine, simulation_time_step, scenario_parame self.direct_route_travel_time = None self.direct_route_travel_distance = None # - self.modal_state = G_RQ_STATE_MONOMODAL # mono-modal trip by default + self.modal_state = RQ_MODAL_STATE.MONOMODAL # mono-modal trip by default def get_rid(self): return self.rid @@ -112,6 +117,9 @@ def get_origin_node(self): def get_destination_node(self): return self.d_node + + def get_modal_state(self) -> RQ_MODAL_STATE: + return self.modal_state def return_offer(self, op_id): return self.offer.get(op_id) @@ -213,17 +221,25 @@ def user_leaves_vehicle(self, simulation_time, do_pos, t_egress): self.do_pos = do_pos self.t_egress = t_egress - def create_SubTripRequest(self, subtrip_id, mod_o_node=None, mod_d_node=None, mod_start_time=None, modal_state = None): + def create_SubTripRequest( + self, + subtrip_id: int, + leg_o_node: tp.Optional[int] = None, + leg_d_node: tp.Optional[int] = None, + leg_start_time: tp.Optional[int] = None, + modal_state: tp.Optional[RQ_MODAL_STATE] = None, + routing_engine : tp.Optional['NetworkBase'] = None, + ): """ this function creates subtriprequests (i.e. a customer sends multiple requests) based on a attributes of itself. different subtrip-customers can vary in start and target node, earlest start time and modal_state (monomodal, firstmile, lastmile, firstlastmile) :param subtrip_id: identifier of the subtrip (this is not the customer id!) :type subtrip_id: int - :param mod_o_node: new origin node index of subtrip - :type mod_o_node: int - :param mod_d_node: new destination node index of subtrip - :type mod_d_node: int - :param mod_start_time: new earliest start time of the trip - :type mod_start_time: int + :param leg_o_node: new origin node index of subtrip + :type leg_o_node: int + :param leg_d_node: new destination node index of subtrip + :type leg_d_node: int + :param leg_start_time: new earliest start time of the trip + :type leg_start_time: int :param modal_state: indicator of modality (indicator if monomodal, first, last or firstlast mile trip) :type modal_state: int in G_RQ_STATE_MONOMODAL, G_RQ_STATE_FIRSTMILE, G_RQ_STATE_LASTMILE, G_RQ_STATE_FIRSTLASTMILE (globals) :return: new traveler with specified attributes @@ -232,14 +248,22 @@ def create_SubTripRequest(self, subtrip_id, mod_o_node=None, mod_d_node=None, mo sub_rq_obj = deepcopy(self) old_rid = sub_rq_obj.get_rid() sub_rq_obj.sub_rid_struct = f"{old_rid}_{subtrip_id}" - if mod_o_node is not None: - sub_rq_obj.o_node = mod_o_node - if mod_d_node is not None: - sub_rq_obj.d_node = mod_d_node - if mod_start_time is not None: - sub_rq_obj.earliest_start_time = mod_start_time + sub_rq_obj.subtrip_id = subtrip_id + if leg_o_node is not None: + sub_rq_obj.o_node = leg_o_node + if leg_d_node is not None: + sub_rq_obj.d_node = leg_d_node + if leg_start_time is not None: + sub_rq_obj.earliest_start_time = leg_start_time if modal_state is not None: sub_rq_obj.modal_state = modal_state + # update travel times and distances + if routing_engine is not None: + sub_rq_obj.o_pos = routing_engine.return_node_position(leg_o_node) + sub_rq_obj.d_pos = routing_engine.return_node_position(leg_d_node) + _, tt, dis = routing_engine.return_travel_costs_1to1(sub_rq_obj.o_pos, sub_rq_obj.d_pos) + sub_rq_obj.direct_route_travel_distance = dis + sub_rq_obj.direct_route_travel_time = tt return sub_rq_obj def set_direct_route_travel_infos(self, routing_engine): @@ -722,6 +746,172 @@ def user_boards_vehicle(self, simulation_time, op_id, vid, pu_pos, t_access): #LOG.info(f"user boards vehicle: {self.rid} | {self.sub_rid_struct} | {self.offer}") self.fare = self.offer[op_id].get(G_OFFER_FARE, 0) return super().user_boards_vehicle(simulation_time, op_id, vid, pu_pos, t_access) + +# -------------------------------------------------------------------------------------------------------------------- # + +INPUT_PARAMETERS_BasicIntermodalRequest = { + "doc" : """This request class is used for intermodal requests. + It is used to model requests that can be served by multiple operators for different modes. + """, + "inherit" : "RequestBase", + "input_parameters_mandatory": [], + "input_parameters_optional": [], + "mandatory_modules": [], + "optional_modules": [] +} + +class BasicIntermodalRequest(RequestBase): + """This request class is used for intermodal requests. + It is used to model requests that can be served by one amod operator and one pt operator.""" + type = "BasicIntermodalRequest" + def __init__(self, rq_row, routing_engine, simulation_time_step, scenario_parameters): + super().__init__(rq_row, routing_engine, simulation_time_step, scenario_parameters) + # intermodal attributes + self.modal_state_int: int = rq_row.get(G_RQ_MODAL_STATE_VALUE, RQ_MODAL_STATE.MONOMODAL.value) # mono-modal trip by default + self.modal_state: RQ_MODAL_STATE = RQ_MODAL_STATE(self.modal_state_int) + self.transfer_station_ids: tp.Optional[tp.List[str]] = self._load_transfer_station_ids(rq_row) + self.max_transfers: int = rq_row.get(G_RQ_MAX_TRANSFERS, 999) # 999 means no limit + self.lastmile_max_wait_time: tp.Optional[int] = rq_row.get(G_IM_LM_WAIT_TIME, None) # the customizable max waiting time for lastmile amod service + self.uncatchable_pt: bool = False # flag for requests that missed their PT connection after FM leg + # PAYG (Plan-As-You-Go) specific attributes + self.payg_interrupted: bool = False # flag for PAYG trips that were interrupted + self.payg_interrupt_state: tp.Optional[int] = None # PAYG_TRIP_STATE value when interrupted + self.payg_interrupt_time: tp.Optional[int] = None # simulation time when interrupted + + def _load_transfer_station_ids(self, rq_row) -> tp.Optional[tp.List[str]]: + raw_transfer_station_ids = rq_row.get(G_RQ_TRANSFER_STATION_IDS, None) + if raw_transfer_station_ids is None or pd.isnull(raw_transfer_station_ids) or raw_transfer_station_ids == "": + return None + else: + return raw_transfer_station_ids.split(";") # in FLM case, two transfer station ids are given + + def get_transfer_station_ids(self) -> tp.Optional[tp.List[str]]: + return self.transfer_station_ids + + def get_max_transfers(self) -> int: + return self.max_transfers + + def get_lastmile_max_wait_time(self) -> tp.Optional[int]: + return self.lastmile_max_wait_time + + def set_uncatchable_pt(self, value: bool): + """Set the uncatchable_pt flag indicating the passenger missed their PT connection.""" + self.uncatchable_pt = value + + def is_uncatchable_pt(self) -> bool: + """Return whether this request missed its PT connection after FM leg.""" + return self.uncatchable_pt + + def set_payg_interrupted(self, interrupted: bool, interrupt_state: tp.Optional[int] = None, interrupt_time: tp.Optional[int] = None): + """Set the PAYG interrupted flag and related information. + + Args: + interrupted: Whether the trip was interrupted + interrupt_state: PAYG_TRIP_STATE value (e.g., -1 for NO_PT, -2 for NO_LM_AMOD) + interrupt_time: Simulation time when the interruption occurred + """ + self.payg_interrupted = interrupted + self.payg_interrupt_state = interrupt_state + self.payg_interrupt_time = interrupt_time + + def is_payg_interrupted(self) -> bool: + """Return whether this PAYG trip was interrupted.""" + return self.payg_interrupted + + def record_data(self): + record_dict = {} + # input + record_dict[G_RQ_ID] = f"{self.rid}" + record_dict[G_RQ_SUB_TRIP_ID] = self.subtrip_id + record_dict[G_RQ_IS_PARENT_REQUEST] = self.sub_rid_struct is None + record_dict[G_RQ_TYPE] = self.type + record_dict[G_RQ_PAX] = self.nr_pax + record_dict[G_RQ_TIME] = self.rq_time + record_dict[G_RQ_EPT] = self.earliest_start_time + # node output + record_dict[G_RQ_ORIGIN] = self.o_node + record_dict[G_RQ_DESTINATION] = self.d_node + # position output + if self.pu_pos is None or self.pu_pos == self.o_pos: + record_dict[G_RQ_PUL] = "" + else: + record_dict[G_RQ_PUL] = return_position_str(self.pu_pos) + if self.do_pos is None or self.do_pos == self.d_pos: + record_dict[G_RQ_DOL] = "" + else: + record_dict[G_RQ_DOL] = return_position_str(self.do_pos) + if self.t_access is None: + record_dict[G_RQ_ACCESS] = "" + else: + record_dict[G_RQ_ACCESS] = self.t_access + if self.t_egress is None: + record_dict[G_RQ_EGRESS] = "" + else: + record_dict[G_RQ_EGRESS] = self.t_egress + if self.direct_route_travel_time is not None: + record_dict[G_RQ_DRT] = self.direct_route_travel_time + if self.direct_route_travel_distance is not None: + record_dict[G_RQ_DRD] = self.direct_route_travel_distance + # offers + all_offer_info = [] + for op_id, operator_offer in self.offer.items(): + all_offer_info.append(f"{op_id}:" + operator_offer.to_output_str()) + record_dict[G_RQ_OFFERS] = "|".join(all_offer_info) + # decision-dependent + record_dict[G_RQ_LEAVE_TIME] = self.leave_system_time + record_dict[G_RQ_CHOSEN_OP_ID] = self.chosen_operator_id + record_dict[G_RQ_OP_ID] = self.service_opid + record_dict[G_RQ_VID] = self.service_vid + record_dict[G_RQ_PU] = self.pu_time + record_dict[G_RQ_DO] = self.do_time + record_dict[G_RQ_FARE] = self.fare + record_dict[G_RQ_MODAL_STATE_VALUE] = self.modal_state_int + record_dict[G_RQ_UNCATCHABLE_PT] = self.uncatchable_pt + # PAYG specific records + record_dict[G_RQ_PAYG_INTERRUPTED] = self.payg_interrupted + record_dict[G_RQ_PAYG_INTERRUPT_STATE] = self.payg_interrupt_state + record_dict[G_RQ_PAYG_INTERRUPT_TIME] = self.payg_interrupt_time + return self._add_record(record_dict) + + def choose_offer(self, scenario_parameters, simulation_time): + """This method returns the operator id of the chosen mode. If both a PT-only and an AMoD+PT offer is available, + the AMoD+PT offer is always chosen. If only one offer is available, this offer is chosen. + For intermodal offers, the operator id is a tuple: ((operator_id, sub_trip_id), ...) + 0..n: MoD fleet provider + None: not decided yet + -1: decline all MoD + -2: PT operator + :param scenario_parameters: scenario parameter dictionary + :param simulation_time: current simulation time + :return: operator_id of chosen offer; or -1 if all MoD offers are declined; None if decision not defined yet + """ + test_all_decline = super().choose_offer(scenario_parameters, simulation_time) + if test_all_decline is not None and test_all_decline < 0: + return -1 + if len(self.offer) == 0: + return None + opts = [offer_id for offer_id, operator_offer in self.offer.items() if + operator_offer is not None and not operator_offer.service_declined()] + if len(opts) == 0: + return None + elif len(opts) == 1: # only one offer: pure amod, pt or amod+pt + self.fare = self.offer[opts[0]].get(G_OFFER_FARE, 0) + self.chosen_operator_id = opts[0] + # offer_id is a tuple: ((operator_id, sub_trip_id), ...) + return opts[0] + elif len(opts) == 2: # two offers: pure pt and amod+pt + # always choose amod+pt + for offer_id, operator_offer in self.offer.items(): + op_id = operator_offer.operator_id + if op_id != -2: + self.fare = operator_offer.get(G_OFFER_FARE, 0) + self.chosen_operator_id = op_id + # offer_id is a tuple: ((operator_id, sub_trip_id), ...) + return offer_id + else: + LOG.error(f"not implemented {offer_str(self.offer)}") + raise NotImplementedError + # -------------------------------------------------------------------------------------------------------------------- # # Parcel Requests # diff --git a/src/demand/demand.py b/src/demand/demand.py index b9182f5d..c7cfd231 100644 --- a/src/demand/demand.py +++ b/src/demand/demand.py @@ -7,12 +7,15 @@ # ------------------------------------------ import numpy as np import pandas as pd +import typing as tp pd.options.mode.chained_assignment = None # src imports # ----------- from src.misc.distributions import draw_from_distribution_dict from src.misc.init_modules import load_request_module +if tp.TYPE_CHECKING: + from src.demand.TravelerModels import RequestBase # global variables # ---------------- @@ -216,6 +219,42 @@ def get_new_travelers(self, simulation_time, *, since=None): list_new_traveler_rid_obj.append((rid, rq)) LOG.debug(f"{len(list_new_traveler_rid_obj)} new travelers join the simulation at time {simulation_time}.") return list_new_traveler_rid_obj + + def create_sub_requests( + self, + rq_obj: 'RequestBase', + subtrip_id: int, + leg_o_node: int, + leg_d_node: int, + leg_start_time: int, + parent_modal_state: RQ_MODAL_STATE, + ) -> 'RequestBase': + """This method creates the sub-requests for the given request. + The new sub-request ids are created by the broker class for the intermodal request. + + Args: + rq_obj (RequestBase): the parent request object + subtrip_id (int): the subtrip id + mod_o_node (int): the origin node of the sub-request + mod_d_node (int): the destination node of the sub-request + mod_start_time (int): the start time of the sub-request + parent_modal_state (RQ_MODAL_STATE): the parent modal state + """ + # TODO: Check if the new sub-request ids should be added to the self.undecided_rq or somewhere else + + sub_rq_obj: 'RequestBase' = rq_obj.create_SubTripRequest( + subtrip_id, + leg_o_node, + leg_d_node, + leg_start_time, + parent_modal_state, + self.routing_engine + ) + # Use the sub_rid_struct as the key for the sub-request: rid_struct = rid_parent + "_" + subtrip_id + sub_rid_struct: str = sub_rq_obj.get_rid_struct() + self.rq_db[sub_rid_struct] = sub_rq_obj + LOG.debug(f"Created sub-request {sub_rid_struct} for request {rq_obj.get_rid_struct()}") + return sub_rq_obj def get_undecided_travelers(self, simulation_time): """This method returns the list of currently undecided requests. @@ -292,7 +331,7 @@ class SlaveDemand(Demand): """This class can be used when request are added from an external demand module.""" rq_class = load_request_module("SlaveRequest") rq_parcel_class = load_request_module("SlaveParcelRequest") - def add_request(self, rq_info_dict, offer_id, routing_engine, sim_time, modal_state = G_RQ_STATE_MONOMODAL): + def add_request(self, rq_info_dict, offer_id, routing_engine, sim_time, modal_state = RQ_MODAL_STATE.MONOMODAL): """ this function is used to add a new (person) request to the demand class :param rq_info_dict: dictionary with all information regarding the request input :param offer_id: used if there are different subrequests (TODO make optional? needed for moia) @@ -304,7 +343,7 @@ def add_request(self, rq_info_dict, offer_id, routing_engine, sim_time, modal_st rq_info_dict[G_RQ_TIME] = sim_time if rq_info_dict.get(G_RQ_LDT) is None: rq_info_dict[G_RQ_LDT] = 0 - if modal_state == G_RQ_STATE_MONOMODAL: + if modal_state == RQ_MODAL_STATE.MONOMODAL: # original request rq_obj = self.rq_class(rq_info_dict, routing_engine, 1, self.scenario_parameters) rq_obj.set_direct_route_travel_infos(routing_engine) diff --git a/src/evaluation/intermodal.py b/src/evaluation/intermodal.py new file mode 100644 index 00000000..b987d4e6 --- /dev/null +++ b/src/evaluation/intermodal.py @@ -0,0 +1,807 @@ +import os +import sys +import glob +import numpy as np +import pandas as pd +import re + +MAIN_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.append(MAIN_DIR) +from src.misc.globals import * + +# Import constants from standard evaluation +EURO_PER_TON_OF_CO2 = 145 # from BVWP2030 Modulhandbuch (page 113) +EMISSION_CPG = 145 * 100 / 1000**2 +ENERGY_EMISSIONS = 112 # g/kWh from https://www.swm.de/dam/swm/dokumente/geschaeftskunden/broschuere-strom-erdgas-gk.pdf +PV_G_CO2_KM = 130 # g/km from https://www.ris-muenchen.de/RII/RII/DOK/ANTRAG/2337762.pdf with 60:38 benzin vs diesel + +def calculate_pt_wait_time(fm_amod_row, pt_row): + """Calculate PT wait time for a given request based on its sub-trips. + Defines wait time as: (PT Pick-up - AMoD Drop-off) - Required Transfer Time + Station Waiting Time. (Note: PT Pick-up refers to the source station departure time). + """ + amod_dropoff_time = fm_amod_row.get(G_RQ_DO, np.nan) + pt_pickup_time = pt_row.get(G_RQ_PU, np.nan) + + pt_offer_str = pt_row.get(G_RQ_OFFERS, None) + if pt_offer_str is not None and not pd.isna(pt_offer_str): + pt_station_wait_time = int(re.search(r't_wait:(\d+)', pt_offer_str).group(1)) + source_walking_time = int(re.search(r'source_walking_time:(\d+)', pt_offer_str).group(1)) + pt_wait_time = (pt_pickup_time - amod_dropoff_time) - source_walking_time + pt_station_wait_time + return max(pt_wait_time, 0) + return np.nan + +def create_parent_user_stats(output_dir, evaluation_start_time=None, evaluation_end_time=None, print_comments=False): + """ + Pre-processing step: Aggregate sub-trips into parent requests. + + Logic: + - Group by request_id + - A parent request is "served" ONLY if all its mandatory legs are served + - Sum/aggregate metrics from sub-trips to parent level + - Save detailed leg information for analysis + + :param output_dir: scenario output directory + :param evaluation_start_time: start time filter + :param evaluation_end_time: end time filter + :param print_comments: print status messages + :return: parent_df - DataFrame with aggregated parent request data + """ + if print_comments: + print("Creating parent user stats from sub-trips...") + + # Read original user stats + user_stats_file = os.path.join(output_dir, "1_user-stats.csv") + df = pd.read_csv(user_stats_file) + + # Apply time filters + if evaluation_start_time is not None: + df = df[df[G_RQ_TIME] >= evaluation_start_time] + if evaluation_end_time is not None: + df = df[df[G_RQ_TIME] < evaluation_end_time] + + # Separate parent requests and sub-trips + parent_df = df[df[G_RQ_IS_PARENT_REQUEST] == True].copy() + subtrip_df = df[df[G_RQ_IS_PARENT_REQUEST] == False].copy() + + if print_comments: + print(f" Found {len(parent_df)} parent requests and {len(subtrip_df)} sub-trips") + + # For each parent request, aggregate sub-trip information + parent_rows = [] + + for parent_idx, parent_row in parent_df.iterrows(): + request_id = parent_row[G_RQ_ID] + modal_state = parent_row.get(G_RQ_MODAL_STATE_VALUE, np.nan) + + # Get all sub-trips for this request + sub_trips = subtrip_df[subtrip_df[G_RQ_ID] == request_id] + + # Create aggregated parent row + parent_agg = parent_row.to_dict() + + # Determine if request is served + # For MONOMODAL (0), parent itself determines if served + # For intermodal (FM=1, LM=2, FLM=3), check if all mandatory sub-trips are served + is_served = False + total_fare = 0 + total_wait_time = 0 + total_travel_time = 0 + + # AMoD-only metrics (excluding PT segments for comparability with standard evaluation) + amod_fare = 0 + amod_wait_time = 0 # Only AMoD wait time (initial wait + LM wait if applicable) + amod_travel_time = 0 # Only AMoD in-vehicle time + amod_direct_distance = 0 # Direct distance for AMoD segments + + # Store leg-specific information + leg_info = {} + + if modal_state == RQ_MODAL_STATE.MONOMODAL.value: + # MONOMODAL: parent row has all information + is_served = not pd.isna(parent_row.get(G_RQ_PU)) + if is_served: + total_fare = parent_row.get(G_RQ_FARE, 0) + total_wait_time = parent_row.get(G_RQ_PU, 0) - parent_row.get(G_RQ_TIME, 0) + total_travel_time = parent_row.get(G_RQ_DO, 0) - parent_row.get(G_RQ_PU, 0) + # AMoD-only metrics (same as total for MONOMODAL) + amod_fare = total_fare + amod_wait_time = total_wait_time + amod_travel_time = total_travel_time + amod_direct_distance = parent_row.get(G_RQ_DRD, 0) + + elif modal_state == RQ_MODAL_STATE.FIRSTMILE.value: + # FM: Need FM_AMOD (1) and FM_PT (2) + fm_amod = sub_trips[sub_trips[G_RQ_SUB_TRIP_ID] == RQ_SUB_TRIP_ID.FM_AMOD.value] + fm_pt = sub_trips[sub_trips[G_RQ_SUB_TRIP_ID] == RQ_SUB_TRIP_ID.FM_PT.value] + + if len(fm_amod) > 0 and len(fm_pt) > 0: + fm_amod_row = fm_amod.iloc[0] + fm_pt_row = fm_pt.iloc[0] + + # Both legs must be served + fm_amod_served = not pd.isna(fm_amod_row.get(G_RQ_PU)) + fm_pt_served = not pd.isna(fm_pt_row.get(G_RQ_PU)) + is_served = fm_amod_served and fm_pt_served + + if is_served: + # Aggregate metrics + total_fare = fm_amod_row.get(G_RQ_FARE, 0) + fm_pt_row.get(G_RQ_FARE, 0) + + # Wait time for FM_AMOD leg + fm_amod_wait = fm_amod_row.get(G_RQ_PU, 0) - fm_amod_row.get(G_RQ_TIME, 0) + + # Wait time for PT leg (waiting for PT after AMoD dropoff) + fm_pt_wait = calculate_pt_wait_time(fm_amod_row, fm_pt_row) + + total_wait_time = fm_amod_wait + fm_pt_wait + + # Total travel time from AMoD pickup to PT drop-off + total_travel_time = fm_pt_row.get(G_RQ_DO, 0) - fm_amod_row.get(G_RQ_PU, 0) + + # AMoD-only metrics (FM has one AMoD leg) + amod_fare = fm_amod_row.get(G_RQ_FARE, 0) + amod_wait_time = fm_amod_wait # Only initial AMoD wait + amod_travel_time = fm_amod_row.get(G_RQ_DO, 0) - fm_amod_row.get(G_RQ_PU, 0) + amod_direct_distance = fm_amod_row.get(G_RQ_DRD, 0) + + # Store leg info + leg_info['fm_amod_pu'] = fm_amod_row.get(G_RQ_PU) + leg_info['fm_amod_do'] = fm_amod_row.get(G_RQ_DO) + leg_info['fm_amod_wait'] = fm_amod_wait + leg_info['fm_amod_fare'] = fm_amod_row.get(G_RQ_FARE, 0) + leg_info['fm_amod_travel_time'] = amod_travel_time + leg_info['fm_amod_direct_distance'] = amod_direct_distance + leg_info['fm_pt_pu'] = fm_pt_row.get(G_RQ_PU) + leg_info['fm_pt_do'] = fm_pt_row.get(G_RQ_DO) + leg_info['fm_pt_wait'] = fm_pt_wait + leg_info['fm_pt_fare'] = fm_pt_row.get(G_RQ_FARE, 0) + + elif modal_state == RQ_MODAL_STATE.LASTMILE.value: + # LM: Need LM_PT (3) and LM_AMOD (4) + lm_pt = sub_trips[sub_trips[G_RQ_SUB_TRIP_ID] == RQ_SUB_TRIP_ID.LM_PT.value] + lm_amod = sub_trips[sub_trips[G_RQ_SUB_TRIP_ID] == RQ_SUB_TRIP_ID.LM_AMOD.value] + + if len(lm_pt) > 0 and len(lm_amod) > 0: + lm_pt_row = lm_pt.iloc[0] + lm_amod_row = lm_amod.iloc[0] + + lm_pt_served = not pd.isna(lm_pt_row.get(G_RQ_PU)) + lm_amod_served = not pd.isna(lm_amod_row.get(G_RQ_PU)) + is_served = lm_pt_served and lm_amod_served + + if is_served: + total_fare = lm_pt_row.get(G_RQ_FARE, 0) + lm_amod_row.get(G_RQ_FARE, 0) + + # Wait time for PT leg + pt_offer_str = lm_pt_row.get(G_RQ_OFFERS, None) + lm_pt_wait = int(re.search(r't_wait:(\d+)', pt_offer_str).group(1)) + + # Wait time for LM AMoD (waiting for AMoD) + lm_pt_target_walking_time = int(re.search(r'target_walking_time:(\d+)', pt_offer_str).group(1)) + lm_amod_wait = lm_amod_row.get(G_RQ_PU, 0) - lm_pt_row.get(G_RQ_DO, 0) - lm_pt_target_walking_time + + total_wait_time = lm_pt_wait + lm_amod_wait + # Total travel time from PT pickup to AMoD drop-off + total_travel_time = lm_amod_row.get(G_RQ_DO, 0) - lm_pt_row.get(G_RQ_PU, 0) + + # AMoD-only metrics (LM has one AMoD leg) + amod_fare = lm_amod_row.get(G_RQ_FARE, 0) + amod_wait_time = lm_amod_wait # AMoD wait after PT + lm_amod_travel_time = lm_amod_row.get(G_RQ_DO, 0) - lm_amod_row.get(G_RQ_PU, 0) + amod_travel_time = lm_amod_travel_time + amod_direct_distance = lm_amod_row.get(G_RQ_DRD, 0) + + # Store leg info + leg_info['lm_pt_pu'] = lm_pt_row.get(G_RQ_PU) + leg_info['lm_pt_do'] = lm_pt_row.get(G_RQ_DO) + leg_info['lm_pt_wait'] = lm_pt_wait + leg_info['lm_pt_fare'] = lm_pt_row.get(G_RQ_FARE, 0) + leg_info['lm_amod_pu'] = lm_amod_row.get(G_RQ_PU) + leg_info['lm_amod_do'] = lm_amod_row.get(G_RQ_DO) + leg_info['lm_amod_wait'] = lm_amod_wait + leg_info['lm_amod_fare'] = lm_amod_row.get(G_RQ_FARE, 0) + leg_info['lm_amod_travel_time'] = lm_amod_travel_time + leg_info['lm_amod_direct_distance'] = amod_direct_distance + + elif modal_state == RQ_MODAL_STATE.FIRSTLASTMILE.value: + # FLM: Need FLM_AMOD_0 (5), FLM_PT (6), FLM_AMOD_1 (7) + flm_amod_0 = sub_trips[sub_trips[G_RQ_SUB_TRIP_ID] == RQ_SUB_TRIP_ID.FLM_AMOD_0.value] + flm_pt = sub_trips[sub_trips[G_RQ_SUB_TRIP_ID] == RQ_SUB_TRIP_ID.FLM_PT.value] + flm_amod_1 = sub_trips[sub_trips[G_RQ_SUB_TRIP_ID] == RQ_SUB_TRIP_ID.FLM_AMOD_1.value] + + if len(flm_amod_0) > 0 and len(flm_pt) > 0 and len(flm_amod_1) > 0: + flm_amod_0_row = flm_amod_0.iloc[0] + flm_pt_row = flm_pt.iloc[0] + flm_amod_1_row = flm_amod_1.iloc[0] + + flm_amod_0_served = not pd.isna(flm_amod_0_row.get(G_RQ_PU)) + flm_pt_served = not pd.isna(flm_pt_row.get(G_RQ_PU)) + flm_amod_1_served = not pd.isna(flm_amod_1_row.get(G_RQ_PU)) + is_served = flm_amod_0_served and flm_pt_served and flm_amod_1_served + + if is_served: + total_fare = (flm_amod_0_row.get(G_RQ_FARE, 0) + + flm_pt_row.get(G_RQ_FARE, 0) + + flm_amod_1_row.get(G_RQ_FARE, 0)) + + # Wait times + flm_amod_0_wait = flm_amod_0_row.get(G_RQ_PU, 0) - flm_amod_0_row.get(G_RQ_TIME, 0) + flm_pt_wait = calculate_pt_wait_time(flm_amod_0_row, flm_pt_row) + flm_pt_offer_str = flm_pt_row.get(G_RQ_OFFERS, None) + flm_pt_target_walking_time = int(re.search(r'target_walking_time:(\d+)', flm_pt_offer_str).group(1)) + # Wait time for FLM_AMOD_1 (waiting for AMoD): LM AMoD pickup - PT drop-off - target walking time + flm_amod_1_wait = flm_amod_1_row.get(G_RQ_PU, 0) - flm_pt_row.get(G_RQ_DO, 0) - flm_pt_target_walking_time + + total_wait_time = flm_amod_0_wait + flm_pt_wait + flm_amod_1_wait + # Total travel time from first AMoD pickup to last AMoD drop-off + total_travel_time = flm_amod_1_row.get(G_RQ_DO, 0) - flm_amod_0_row.get(G_RQ_PU, 0) + + # AMoD-only metrics (FLM has two AMoD legs) + amod_fare = flm_amod_0_row.get(G_RQ_FARE, 0) + flm_amod_1_row.get(G_RQ_FARE, 0) + amod_wait_time = flm_amod_0_wait + flm_amod_1_wait # Both AMoD wait times + flm_amod_0_travel_time = flm_amod_0_row.get(G_RQ_DO, 0) - flm_amod_0_row.get(G_RQ_PU, 0) + flm_amod_1_travel_time = flm_amod_1_row.get(G_RQ_DO, 0) - flm_amod_1_row.get(G_RQ_PU, 0) + amod_travel_time = flm_amod_0_travel_time + flm_amod_1_travel_time + amod_direct_distance = flm_amod_0_row.get(G_RQ_DRD, 0) + flm_amod_1_row.get(G_RQ_DRD, 0) + + # Store leg info + leg_info['flm_amod_0_pu'] = flm_amod_0_row.get(G_RQ_PU) + leg_info['flm_amod_0_do'] = flm_amod_0_row.get(G_RQ_DO) + leg_info['flm_amod_0_wait'] = flm_amod_0_wait + leg_info['flm_amod_0_fare'] = flm_amod_0_row.get(G_RQ_FARE, 0) + leg_info['flm_amod_0_travel_time'] = flm_amod_0_travel_time + leg_info['flm_amod_0_direct_distance'] = flm_amod_0_row.get(G_RQ_DRD, 0) + leg_info['flm_pt_pu'] = flm_pt_row.get(G_RQ_PU) + leg_info['flm_pt_do'] = flm_pt_row.get(G_RQ_DO) + leg_info['flm_pt_wait'] = flm_pt_wait + leg_info['flm_pt_fare'] = flm_pt_row.get(G_RQ_FARE, 0) + leg_info['flm_amod_1_pu'] = flm_amod_1_row.get(G_RQ_PU) + leg_info['flm_amod_1_do'] = flm_amod_1_row.get(G_RQ_DO) + leg_info['flm_amod_1_wait'] = flm_amod_1_wait + leg_info['flm_amod_1_fare'] = flm_amod_1_row.get(G_RQ_FARE, 0) + leg_info['flm_amod_1_travel_time'] = flm_amod_1_travel_time + leg_info['flm_amod_1_direct_distance'] = flm_amod_1_row.get(G_RQ_DRD, 0) + + # Update parent aggregation with computed values + parent_agg['is_served'] = is_served + parent_agg['total_fare'] = total_fare if is_served else np.nan + parent_agg['total_wait_time'] = total_wait_time if is_served else np.nan + parent_agg['total_travel_time'] = total_travel_time if is_served else np.nan + + # AMoD-only metrics (excluding PT segments for comparability with standard evaluation) + parent_agg['amod_fare'] = amod_fare if is_served else np.nan + parent_agg['amod_wait_time'] = amod_wait_time if is_served else np.nan + parent_agg['amod_travel_time'] = amod_travel_time if is_served else np.nan + parent_agg['amod_direct_distance'] = amod_direct_distance if is_served else np.nan + + # Add leg info as columns + for key, val in leg_info.items(): + parent_agg[key] = val + + parent_rows.append(parent_agg) + + # Create parent DataFrame + parent_result_df = pd.DataFrame(parent_rows) + + # Save to file + parent_output_file = os.path.join(output_dir, "1_user-stats_parent.csv") + parent_result_df.to_csv(parent_output_file, index=False) + + if print_comments: + print(f" Saved parent user stats to: {parent_output_file}") + print(f" Served requests: {parent_result_df['is_served'].sum()} / {len(parent_result_df)}") + + return parent_result_df + + +def categorize_modal_state(modal_state_value): + """Categorize request based on modal state value.""" + if pd.isna(modal_state_value): + return 'Unknown' + modal_state_value = int(modal_state_value) + if modal_state_value == RQ_MODAL_STATE.MONOMODAL.value: + return 'DRT_only' + elif modal_state_value == RQ_MODAL_STATE.FIRSTMILE.value: + return 'FM' + elif modal_state_value == RQ_MODAL_STATE.LASTMILE.value: + return 'LM' + elif modal_state_value == RQ_MODAL_STATE.FIRSTLASTMILE.value: + return 'FLM' + elif modal_state_value == RQ_MODAL_STATE.PT.value: + return 'PT_only' + else: + return 'Unknown' + + +def calculate_payg_metrics(parent_user_stats, is_payg_scenario=False, print_comments=False): + """ + Calculate PAYG-specific metrics from parent user stats. + + PAYG (Plan-As-You-Go) trips may be interrupted if: + - No PT available after FM alighting (INTERRUPTED_NO_PT) + - No LM AMoD available after PT alighting (INTERRUPTED_NO_LM_AMOD) + + :param parent_user_stats: DataFrame with parent request data + :param is_payg_scenario: whether this is a PAYG scenario (determined by broker_type in config) + :param print_comments: whether to print status messages + :return: dict with PAYG-specific metrics, empty dict if not a PAYG scenario + """ + payg_stats = {} + + # Check if this is a PAYG scenario based on broker type from config + if not is_payg_scenario: + return {} # Not a PAYG scenario + + # Check if PAYG columns exist (for backwards compatibility) + if G_RQ_PAYG_INTERRUPTED not in parent_user_stats.columns: + if print_comments: + print(" Warning: PAYG scenario but no payg_interrupted column found") + return {} + + # Filter to intermodal requests only (FM, LM, FLM) - these are the ones that can be interrupted + intermodal_mask = parent_user_stats[G_RQ_MODAL_STATE_VALUE].isin([ + RQ_MODAL_STATE.FIRSTMILE.value, + RQ_MODAL_STATE.LASTMILE.value, + RQ_MODAL_STATE.FIRSTLASTMILE.value + ]) + intermodal_df = parent_user_stats[intermodal_mask].copy() + + total_intermodal = len(intermodal_df) + if total_intermodal == 0: + return {} + + # Count interrupted requests + interrupted_df = intermodal_df[intermodal_df[G_RQ_PAYG_INTERRUPTED] == True] + interrupted_count = len(interrupted_df) + completed_count = total_intermodal - interrupted_count + + # Overall rates + payg_stats['payg_total_intermodal_requests'] = total_intermodal + payg_stats['payg_completed_count'] = completed_count + payg_stats['payg_interrupted_count'] = interrupted_count + payg_stats['payg_completion_rate [%]'] = completed_count / total_intermodal * 100 + payg_stats['payg_interrupt_rate [%]'] = interrupted_count / total_intermodal * 100 + + # Breakdown by interrupt state + if G_RQ_PAYG_INTERRUPT_STATE in interrupted_df.columns: + no_pt_count = len(interrupted_df[ + interrupted_df[G_RQ_PAYG_INTERRUPT_STATE] == PAYG_TRIP_STATE.INTERRUPTED_NO_PT.value + ]) + no_amod_count = len(interrupted_df[ + interrupted_df[G_RQ_PAYG_INTERRUPT_STATE] == PAYG_TRIP_STATE.INTERRUPTED_NO_LM_AMOD.value + ]) + else: + no_pt_count = 0 + no_amod_count = 0 + + payg_stats['payg_interrupt_no_pt_count'] = no_pt_count + payg_stats['payg_interrupt_no_pt_rate [%]'] = no_pt_count / total_intermodal * 100 if total_intermodal > 0 else 0 + payg_stats['payg_interrupt_no_amod_count'] = no_amod_count + payg_stats['payg_interrupt_no_amod_rate [%]'] = no_amod_count / total_intermodal * 100 if total_intermodal > 0 else 0 + + # Breakdown by modal state + for category, modal_value in [('FM', RQ_MODAL_STATE.FIRSTMILE.value), + ('LM', RQ_MODAL_STATE.LASTMILE.value), + ('FLM', RQ_MODAL_STATE.FIRSTLASTMILE.value)]: + cat_df = intermodal_df[intermodal_df[G_RQ_MODAL_STATE_VALUE] == modal_value] + cat_total = len(cat_df) + if cat_total > 0: + cat_interrupted = len(cat_df[cat_df[G_RQ_PAYG_INTERRUPTED] == True]) + payg_stats[f'payg_{category}_total'] = cat_total + payg_stats[f'payg_{category}_interrupted_count'] = cat_interrupted + payg_stats[f'payg_{category}_interrupt_rate [%]'] = cat_interrupted / cat_total * 100 + else: + payg_stats[f'payg_{category}_total'] = 0 + payg_stats[f'payg_{category}_interrupted_count'] = 0 + payg_stats[f'payg_{category}_interrupt_rate [%]'] = np.nan + + if print_comments and interrupted_count > 0: + print(f" PAYG Interruptions: {interrupted_count}/{total_intermodal} ({payg_stats['payg_interrupt_rate [%]']:.1f}%)") + print(f" - No PT available: {no_pt_count}") + print(f" - No LM AMoD available: {no_amod_count}") + + return payg_stats + + +def intermodal_evaluation(output_dir, evaluation_start_time=None, evaluation_end_time=None, print_comments=False, dir_names_in={}): + """ + Main intermodal evaluation function. + + Follows the same pattern as standard_evaluation but: + 1. Pre-processes user stats to create parent request aggregations + 2. Calculates standard metrics on parent requests + 3. Adds intermodal-specific metrics + + :param output_dir: scenario output directory + :param evaluation_start_time: start time filter + :param evaluation_end_time: end time filter + :param print_comments: print status messages + :param dir_names_in: directory dictionary (optional) + :return: result DataFrame + """ + if not os.path.isdir(output_dir): + raise IOError(f"Could not find result directory {output_dir}!") + + # Load scenario configuration + scenario_parameters, list_operator_attributes, _ = load_scenario_inputs(output_dir) + dir_names = get_directory_dict(scenario_parameters, list_operator_attributes, abs_fleetpy_dir=MAIN_DIR) + if dir_names_in: + dir_names = dir_names_in + + # Evaluation interval + if evaluation_start_time is None and scenario_parameters.get(G_EVAL_INT_START) is not None: + evaluation_start_time = int(scenario_parameters[G_EVAL_INT_START]) + if evaluation_end_time is None and scenario_parameters.get(G_EVAL_INT_END) is not None: + evaluation_end_time = int(scenario_parameters[G_EVAL_INT_END]) + + # Vehicle type data + from src.evaluation.standard import create_vehicle_type_db, read_op_output_file, avg_in_vehicle_distance, shared_rides + veh_type_db = create_vehicle_type_db(dir_names[G_DIR_VEH]) + veh_type_stats = pd.read_csv(os.path.join(output_dir, "2_vehicle_types.csv")) + + if print_comments: + print(f"Evaluating {scenario_parameters[G_SCENARIO_NAME]}") + print("="*80) + + # Step 1: Create parent user stats + parent_user_stats = create_parent_user_stats(output_dir, evaluation_start_time, evaluation_end_time, print_comments) + + # Add passenger column if needed + if G_RQ_PAX not in parent_user_stats.columns: + parent_user_stats[G_RQ_PAX] = 1 + + # Filter to only served requests for metrics + served_requests = parent_user_stats[parent_user_stats['is_served'] == True].copy() + + if print_comments: + print(f"\nCalculating metrics for {len(served_requests)} served requests...") + + # Categorize requests by modal state + served_requests['modal_category'] = served_requests[G_RQ_MODAL_STATE_VALUE].apply(categorize_modal_state) + + # Total counts + total_requests = len(parent_user_stats) + total_served = len(served_requests) + total_pax = parent_user_stats[G_RQ_PAX].sum() + total_served_pax = served_requests[G_RQ_PAX].sum() + + # Category breakdown + category_counts = served_requests.groupby('modal_category').size() + category_pax = served_requests.groupby('modal_category')[G_RQ_PAX].sum() + + # Calculate service rates + service_rate_overall = total_served / total_requests * 100 if total_requests > 0 else 0 + service_rate_pax = total_served_pax / total_pax * 100 if total_pax > 0 else 0 + + # Service rates by category + service_rates = {} + for category in ['DRT_only', 'FM', 'LM', 'FLM', 'PT_only']: + cat_total = len(parent_user_stats[parent_user_stats[G_RQ_MODAL_STATE_VALUE].apply(categorize_modal_state) == category]) + cat_served = category_counts.get(category, 0) + service_rates[f'{category}_service_rate'] = cat_served / cat_total * 100 if cat_total > 0 else np.nan + service_rates[f'{category}_count'] = cat_served + + # Calculate uncatchable PT statistics (requests that missed their PT connection after FM leg) + uncatchable_stats = {} + if G_RQ_UNCATCHABLE_PT in parent_user_stats.columns: + # Count uncatchable requests by category (only FM and FLM can be uncatchable) + fm_uncatchable = parent_user_stats[ + (parent_user_stats[G_RQ_MODAL_STATE_VALUE].apply(categorize_modal_state) == 'FM') & + (parent_user_stats[G_RQ_UNCATCHABLE_PT] == True) + ] + flm_uncatchable = parent_user_stats[ + (parent_user_stats[G_RQ_MODAL_STATE_VALUE].apply(categorize_modal_state) == 'FLM') & + (parent_user_stats[G_RQ_UNCATCHABLE_PT] == True) + ] + total_uncatchable = len(fm_uncatchable) + len(flm_uncatchable) + + uncatchable_stats['FM_uncatchable_count'] = len(fm_uncatchable) + uncatchable_stats['FLM_uncatchable_count'] = len(flm_uncatchable) + uncatchable_stats['total_uncatchable_count'] = total_uncatchable + + # Calculate uncatchable rate (as % of FM+FLM requests) + fm_total = len(parent_user_stats[parent_user_stats[G_RQ_MODAL_STATE_VALUE].apply(categorize_modal_state) == 'FM']) + flm_total = len(parent_user_stats[parent_user_stats[G_RQ_MODAL_STATE_VALUE].apply(categorize_modal_state) == 'FLM']) + intermodal_with_fm_total = fm_total + flm_total + + uncatchable_stats['FM_uncatchable_rate'] = len(fm_uncatchable) / fm_total * 100 if fm_total > 0 else np.nan + uncatchable_stats['FLM_uncatchable_rate'] = len(flm_uncatchable) / flm_total * 100 if flm_total > 0 else np.nan + uncatchable_stats['total_uncatchable_rate'] = total_uncatchable / intermodal_with_fm_total * 100 if intermodal_with_fm_total > 0 else np.nan + + if print_comments and total_uncatchable > 0: + print(f" Uncatchable PT requests: {total_uncatchable} (FM: {len(fm_uncatchable)}, FLM: {len(flm_uncatchable)})") + else: + # No uncatchable_pt column, set defaults + uncatchable_stats = { + 'FM_uncatchable_count': 0, + 'FLM_uncatchable_count': 0, + 'total_uncatchable_count': 0, + 'FM_uncatchable_rate': np.nan, + 'FLM_uncatchable_rate': np.nan, + 'total_uncatchable_rate': np.nan + } + + # Calculate PAYG-specific metrics (if applicable) + # Determine if this is a PAYG scenario based on broker_type in config + broker_type = scenario_parameters.get(G_BROKER_TYPE, "") + is_payg_scenario = broker_type == "PTBrokerPAYG" + payg_stats = calculate_payg_metrics(parent_user_stats, is_payg_scenario, print_comments) + + # Calculate PT wait times for FM and FLM + fm_requests = served_requests[served_requests['modal_category'] == 'FM'] + flm_requests = served_requests[served_requests['modal_category'] == 'FLM'] + + pt_wait_time_fm = fm_requests['fm_pt_wait'].mean() if len(fm_requests) > 0 and 'fm_pt_wait' in fm_requests.columns else np.nan + pt_wait_time_flm = flm_requests['flm_pt_wait'].mean() if len(flm_requests) > 0 and 'flm_pt_wait' in flm_requests.columns else np.nan + + # Combined PT wait time + pt_waits = [] + if 'fm_pt_wait' in fm_requests.columns: + pt_waits.extend(fm_requests['fm_pt_wait'].dropna().tolist()) + if 'flm_pt_wait' in flm_requests.columns: + pt_waits.extend(flm_requests['flm_pt_wait'].dropna().tolist()) + pt_wait_time_combined = np.mean(pt_waits) if len(pt_waits) > 0 else np.nan + + # Calculate LM wait times for LM and FLM + lm_requests = served_requests[served_requests['modal_category'] == 'LM'] + + lm_wait_time_lm = lm_requests['lm_amod_wait'].mean() if len(lm_requests) > 0 and 'lm_amod_wait' in lm_requests.columns else np.nan + lm_wait_time_flm = flm_requests['flm_amod_1_wait'].mean() if len(flm_requests) > 0 and 'flm_amod_1_wait' in flm_requests.columns else np.nan + + # Combined LM wait time + lm_waits = [] + if 'lm_amod_wait' in lm_requests.columns: + lm_waits.extend(lm_requests['lm_amod_wait'].dropna().tolist()) + if 'flm_amod_1_wait' in flm_requests.columns: + lm_waits.extend(flm_requests['flm_amod_1_wait'].dropna().tolist()) + lm_wait_time_combined = np.mean(lm_waits) if len(lm_waits) > 0 else np.nan + + # Overall metrics + avg_wait_time = served_requests['total_wait_time'].mean() + med_wait_time = served_requests['total_wait_time'].median() + quantile_90_wait_time = served_requests['total_wait_time'].quantile(q=0.9) + avg_travel_time = served_requests['total_travel_time'].mean() + total_revenue = served_requests['total_fare'].sum() + + # AMoD-only metrics (excluding PT segments for comparability with standard evaluation) + amod_avg_wait_time = served_requests['amod_wait_time'].mean() + amod_med_wait_time = served_requests['amod_wait_time'].median() + amod_quantile_90_wait_time = served_requests['amod_wait_time'].quantile(q=0.9) + amod_avg_travel_time = served_requests['amod_travel_time'].mean() + amod_total_revenue = served_requests['amod_fare'].sum() + amod_total_direct_distance = served_requests['amod_direct_distance'].sum() / 1000.0 # Convert to km + + # Detour time calculation (AMoD segments only, excluding PT) + # Detour = actual_travel_time - direct_route_time - boarding_time + # Get boarding time from operator attributes (will be set later when processing operators) + boarding_time = scenario_parameters.get("op_const_boarding_time", 30) # Default 30s + + # Calculate detour for each request based on AMoD segments + # For requests with direct_route_time available + if G_RQ_DRT in served_requests.columns: + # MONOMODAL: use parent's direct route time + monomodal_mask = served_requests['modal_category'] == 'DRT_only' + served_requests.loc[monomodal_mask, 'amod_direct_route_time'] = served_requests.loc[monomodal_mask, G_RQ_DRT] + + # Calculate detour time for AMoD segments + served_requests['amod_detour_time'] = served_requests['amod_travel_time'] - served_requests.get('amod_direct_route_time', served_requests['amod_travel_time']) - boarding_time + # For intermodal, estimate direct route time from direct distance (assuming avg speed ~30 km/h = 8.33 m/s) + avg_speed_ms = 8.33 # m/s, approximately 30 km/h + served_requests.loc[served_requests['amod_detour_time'].isna(), 'amod_detour_time'] = ( + served_requests.loc[served_requests['amod_detour_time'].isna(), 'amod_travel_time'] - + served_requests.loc[served_requests['amod_detour_time'].isna(), 'amod_direct_distance'] / avg_speed_ms - boarding_time + ) + + amod_avg_detour_time = served_requests['amod_detour_time'].mean() + + # Relative detour (percentage) + served_requests['amod_rel_detour'] = ( + (served_requests['amod_travel_time'] - boarding_time - served_requests['amod_direct_distance'] / avg_speed_ms) / + (served_requests['amod_direct_distance'] / avg_speed_ms) + ) * 100.0 + amod_avg_rel_detour = served_requests['amod_rel_detour'].mean() + + # Standard metrics (matching standard_eval.csv format) + result_dict = { + 'operator_id': -3, # Intermodal operator + 'number users': total_served, + 'number travelers': total_served_pax, + 'modal split': service_rate_pax / 100, + 'modal split rq': service_rate_overall / 100, + 'Service_Rate [%]': service_rate_overall, + 'Service_Rate_Pax [%]': service_rate_pax, + + # Category breakdown + 'DRT_only_count': service_rates.get('DRT_only_count', 0), + 'FM_count': service_rates.get('FM_count', 0), + 'LM_count': service_rates.get('LM_count', 0), + 'FLM_count': service_rates.get('FLM_count', 0), + 'PT_only_count': service_rates.get('PT_only_count', 0), + + 'DRT_only_service_rate [%]': service_rates.get('DRT_only_service_rate', np.nan), + 'FM_service_rate [%]': service_rates.get('FM_service_rate', np.nan), + 'LM_service_rate [%]': service_rates.get('LM_service_rate', np.nan), + 'FLM_service_rate [%]': service_rates.get('FLM_service_rate', np.nan), + 'PT_only_service_rate [%]': service_rates.get('PT_only_service_rate', np.nan), + + # Uncatchable PT statistics (requests that missed their PT connection after FM leg) + 'FM_uncatchable_count': uncatchable_stats.get('FM_uncatchable_count', 0), + 'FLM_uncatchable_count': uncatchable_stats.get('FLM_uncatchable_count', 0), + 'total_uncatchable_count': uncatchable_stats.get('total_uncatchable_count', 0), + 'FM_uncatchable_rate [%]': uncatchable_stats.get('FM_uncatchable_rate', np.nan), + 'FLM_uncatchable_rate [%]': uncatchable_stats.get('FLM_uncatchable_rate', np.nan), + 'total_uncatchable_rate [%]': uncatchable_stats.get('total_uncatchable_rate', np.nan), + + # Wait times (total, including PT wait) + 'waiting time': avg_wait_time, + 'waiting time (median)': med_wait_time, + 'waiting time (90% quantile)': quantile_90_wait_time, + 'PT_Wait_Time_FM [s]': pt_wait_time_fm, + 'PT_Wait_Time_FLM [s]': pt_wait_time_flm, + 'PT_Wait_Time_Combined [s]': pt_wait_time_combined, + 'LM_Wait_Time_LM [s]': lm_wait_time_lm, + 'LM_Wait_Time_FLM [s]': lm_wait_time_flm, + 'LM_Wait_Time_Combined [s]': lm_wait_time_combined, + + # AMoD-only metrics (excluding PT, for comparability with standard evaluation) + 'amod_waiting_time': amod_avg_wait_time, + 'amod_waiting_time (median)': amod_med_wait_time, + 'amod_waiting_time (90% quantile)': amod_quantile_90_wait_time, + 'amod_travel_time': amod_avg_travel_time, + 'amod_detour_time': amod_avg_detour_time, + 'amod_rel_detour [%]': amod_avg_rel_detour, + 'amod_revenue': amod_total_revenue, + 'amod_customer_direct_distance [km]': amod_total_direct_distance, + + # Travel metrics + 'travel time': avg_travel_time, + 'mod revenue': total_revenue, + } + + # Add PAYG-specific metrics if available + if payg_stats: + result_dict.update(payg_stats) + + # Vehicle-level analysis for AMoD operators + if print_comments: + print("\nAnalyzing vehicle operations...") + + # Process each AMoD operator + for op_id in range(scenario_parameters.get(G_NR_OPERATORS, 0)): + try: + op_vehicle_df = read_op_output_file(output_dir, op_id, evaluation_start_time, evaluation_end_time) + operator_attributes = list_operator_attributes[int(op_id)] + + if print_comments: + print(f" Processing operator {op_id}: {op_vehicle_df.shape[0]} vehicle route legs") + + # Fleet metrics + n_vehicles = veh_type_stats[veh_type_stats[G_V_OP_ID] == op_id].shape[0] + sim_end_time = scenario_parameters["end_time"] + simulation_time = scenario_parameters["end_time"] - scenario_parameters["start_time"] + + # Utilization + op_vehicle_df["VRL_end_sim_end_time"] = np.minimum(op_vehicle_df[G_VR_LEG_END_TIME], sim_end_time) + op_vehicle_df["VRL_start_sim_end_time"] = np.minimum(op_vehicle_df[G_VR_LEG_START_TIME], sim_end_time) + utilized_veh_df = op_vehicle_df[(op_vehicle_df["status"] != VRL_STATES.OUT_OF_SERVICE.display_name) & + (op_vehicle_df["status"] != VRL_STATES.CHARGING.display_name)] + utilization_time = utilized_veh_df["VRL_end_sim_end_time"].sum() - utilized_veh_df["VRL_start_sim_end_time"].sum() + unutilized_veh_df = op_vehicle_df[(op_vehicle_df["status"] == VRL_STATES.OUT_OF_SERVICE.display_name) | + (op_vehicle_df["status"] == VRL_STATES.CHARGING.display_name)] + unutilized_time = unutilized_veh_df["VRL_end_sim_end_time"].sum() - unutilized_veh_df["VRL_start_sim_end_time"].sum() + + fleet_utilization = 100 * (utilization_time / (n_vehicles * simulation_time - unutilized_time)) if (n_vehicles * simulation_time - unutilized_time) > 0 else 0 + + # Distance metrics + total_km = op_vehicle_df[G_VR_LEG_DISTANCE].sum() / 1000.0 + + def weight_ob_pax(entries): + try: + return entries[G_VR_NR_PAX] * entries[G_VR_LEG_DISTANCE] + except: + return 0.0 + + op_vehicle_df["weighted_ob_pax"] = op_vehicle_df.apply(weight_ob_pax, axis=1) + distance_avg_occupancy = op_vehicle_df["weighted_ob_pax"].sum() / op_vehicle_df[G_VR_LEG_DISTANCE].sum() if op_vehicle_df[G_VR_LEG_DISTANCE].sum() > 0 else 0 + + empty_df = op_vehicle_df[op_vehicle_df[G_VR_OB_RID].isnull()] + empty_vkm = empty_df[G_VR_LEG_DISTANCE].sum() / 1000.0 / total_km * 100.0 if total_km > 0 else 0 + + # Repositioning VKM (new) + repositioning_df = empty_df[empty_df[G_VR_STATUS] == "reposition"] + repositioning_vkm = repositioning_df[G_VR_LEG_DISTANCE].sum() / 1000.0 / total_km * 100.0 if total_km > 0 else 0 + + # Revenue metrics + rev_df = op_vehicle_df[op_vehicle_df["status"].isin([x.display_name for x in G_REVENUE_STATUS])] + vehicle_revenue_hours = (rev_df["VRL_end_sim_end_time"].sum() - rev_df["VRL_start_sim_end_time"].sum()) / 3600.0 + + # Rides per vehicle revenue hours (new) + rides_per_veh_rev_hours = total_served_pax / vehicle_revenue_hours if vehicle_revenue_hours > 0 else 0 + rides_per_veh_rev_hours_rq = total_served / vehicle_revenue_hours if vehicle_revenue_hours > 0 else 0 + + # Shared rides and customer in-vehicle distance (new) + op_shared_rides = shared_rides(op_vehicle_df) + op_customer_in_vehicle_distance = avg_in_vehicle_distance(op_vehicle_df) + + # By-vehicle stats + op_veh_types = veh_type_stats[veh_type_stats[G_V_OP_ID] == op_id] + op_veh_types.set_index(G_V_VID, inplace=True) + all_vid_dict = {} + + for vid, vid_vtype_row in op_veh_types.iterrows(): + vtype_data = veh_type_db[vid_vtype_row[G_V_TYPE]] + op_vid_vehicle_df = op_vehicle_df[op_vehicle_df[G_V_VID] == vid] + veh_km = op_vid_vehicle_df[G_VR_LEG_DISTANCE].sum() / 1000 + veh_kWh = veh_km * vtype_data[G_VTYPE_BATTERY_SIZE] / vtype_data[G_VTYPE_RANGE] + co2_per_kWh = scenario_parameters.get(G_ENERGY_EMISSIONS, ENERGY_EMISSIONS) + if co2_per_kWh is None: + co2_per_kWh = ENERGY_EMISSIONS + veh_co2 = co2_per_kWh * veh_kWh + veh_fix_costs = np.rint(scenario_parameters.get(G_OP_SHARE_FC, 1.0) * vtype_data[G_VTYPE_FIX_COST]) + veh_var_costs = np.rint(vtype_data[G_VTYPE_DIST_COST] * veh_km) + + all_vid_dict[vid] = { + "type": vtype_data[G_VTYPE_NAME], + "total km": veh_km, + "total kWh": veh_kWh, + "total CO2 [g]": veh_co2, + "fix costs": veh_fix_costs, + "total variable costs": veh_var_costs + } + + # Save vehicle-level stats + all_vid_df = pd.DataFrame.from_dict(all_vid_dict, orient="index") + all_vid_df.to_csv(os.path.join(output_dir, f"standard_mod-{op_id}_veh_eval.csv")) + + # Aggregate vehicle metrics + total_co2 = all_vid_df["total CO2 [g]"].sum() if len(all_vid_df) > 0 else 0 + fix_costs = all_vid_df["fix costs"].sum() if len(all_vid_df) > 0 else 0 + var_costs = all_vid_df["total variable costs"].sum() if len(all_vid_df) > 0 else 0 + + # External emission costs (new) + external_emission_costs = np.rint(EMISSION_CPG * total_co2) + + # Add to result dict with operator prefix + result_dict[f'op{op_id}_fleet_utilization [%]'] = fleet_utilization + result_dict[f'op{op_id}_total_vkm'] = total_km + result_dict[f'op{op_id}_occupancy'] = distance_avg_occupancy + result_dict[f'op{op_id}_empty_vkm [%]'] = empty_vkm + result_dict[f'op{op_id}_repositioning_vkm [%]'] = repositioning_vkm + result_dict[f'op{op_id}_vehicle_revenue_hours'] = vehicle_revenue_hours + result_dict[f'op{op_id}_rides_per_veh_rev_hours'] = rides_per_veh_rev_hours + result_dict[f'op{op_id}_rides_per_veh_rev_hours_rq'] = rides_per_veh_rev_hours_rq + result_dict[f'op{op_id}_total_CO2_emissions [t]'] = total_co2 / 10**6 + result_dict[f'op{op_id}_external_emission_costs'] = external_emission_costs + result_dict[f'op{op_id}_fix_costs'] = fix_costs + result_dict[f'op{op_id}_var_costs'] = var_costs + result_dict[f'op{op_id}_shared_rides [%]'] = op_shared_rides + result_dict[f'op{op_id}_customer_in_vehicle_distance'] = op_customer_in_vehicle_distance + + except FileNotFoundError: + if print_comments: + print(f" No vehicle data found for operator {op_id}") + continue + + # Create result DataFrame + result_df = pd.DataFrame([result_dict], index=['Intermodal']).T + result_df.columns = ['Intermodal'] + + # Save to standard_eval.csv for consistency + output_file = os.path.join(output_dir, "standard_eval.csv") + result_df.to_csv(output_file) + + if print_comments: + print(f"\nEvaluation complete! Results saved to: {output_file}") + print("="*80) + + return result_df + + +if __name__ == "__main__": + import sys + if len(sys.argv) > 1: + sc = sys.argv[1] + intermodal_evaluation(sc, print_comments=True) + else: + print("Usage: python intermodal.py ") + print("Example: python src/evaluation/intermodal.py studies/example_study/results/example_im_ptbroker") diff --git a/src/fleetctrl/BrokerAndExchangeFleetControl.py b/src/fleetctrl/BrokerAndExchangeFleetControl.py index 1f4b0e0f..5dfa1970 100644 --- a/src/fleetctrl/BrokerAndExchangeFleetControl.py +++ b/src/fleetctrl/BrokerAndExchangeFleetControl.py @@ -19,7 +19,7 @@ if TYPE_CHECKING: from src.fleetctrl.planning.VehiclePlan import VehiclePlan from src.simulation.Vehicles import SimulationVehicle - from src.routing.NetworkBase import NetworkBase + from src.routing.road.NetworkBase import NetworkBase from src.infra.Zoning import ZoneSystem LOG = logging.getLogger(__name__) diff --git a/src/fleetctrl/FleetControlBase.py b/src/fleetctrl/FleetControlBase.py index bcad9548..11007018 100644 --- a/src/fleetctrl/FleetControlBase.py +++ b/src/fleetctrl/FleetControlBase.py @@ -30,7 +30,7 @@ load_dynamic_fleet_sizing_strategy, load_dynamic_pricing_strategy, load_reservation_strategy from src.fleetctrl.pooling.GeneralPoolingFunctions import get_assigned_rids_from_vehplan if TYPE_CHECKING: - from src.routing.NetworkBase import NetworkBase + from src.routing.road.NetworkBase import NetworkBase from src.simulation.Vehicles import SimulationVehicle from src.infra.Zoning import ZoneSystem from src.infra.ChargingInfrastructure import OperatorChargingAndDepotInfrastructure, PublicChargingInfrastructureOperator diff --git a/src/fleetctrl/PoolingIRSOnly.py b/src/fleetctrl/PoolingIRSOnly.py index 0527c0a1..b7afbb73 100644 --- a/src/fleetctrl/PoolingIRSOnly.py +++ b/src/fleetctrl/PoolingIRSOnly.py @@ -62,6 +62,8 @@ def __init__(self, op_id, operator_attributes, list_vehicles, routing_engine, zo self.tmp_assignment = {} # rid -> VehiclePlan self._init_dynamic_fleetcontrol_output_key(G_FCTRL_CT_RQU) + self.flm_excluded_vid = {} # flm rid -> [vid] + def receive_status_update(self, vid, simulation_time, list_finished_VRL, force_update=True): """This method can be used to update plans and trigger processes whenever a simulation vehicle finished some VehicleRouteLegs. @@ -83,7 +85,7 @@ def receive_status_update(self, vid, simulation_time, list_finished_VRL, force_u self.pos_veh_dict[veh_obj.pos] = [veh_obj] LOG.debug(f"veh {veh_obj} | after status update: {self.veh_plans[vid]}") - def user_request(self, rq, sim_time): + def user_request(self, rq, sim_time, max_wait_time=None): """This method is triggered for a new incoming request. It generally adds the rq to the database. It has to return an offer to the user. This operator class only works with immediate responses and therefore either sends an offer or a rejection. @@ -92,14 +94,18 @@ def user_request(self, rq, sim_time): :type rq: RequestDesign :param sim_time: current simulation time :type sim_time: float + :param max_wait_time: maximum wait time (for LM leg of intermodal requests); None if not specified + :type max_wait_time: float or None :return: offer :rtype: TravellerOffer """ t0 = time.perf_counter() LOG.debug(f"Incoming request {rq.__dict__} at time {sim_time}") self.sim_time = sim_time + if max_wait_time is None: # if not specified, use operator default settings + max_wait_time = self.max_wait_time prq = PlanRequest(rq, self.routing_engine, min_wait_time=self.min_wait_time, - max_wait_time=self.max_wait_time, + max_wait_time=max_wait_time, max_detour_time_factor=self.max_dtf, max_constant_detour_time=self.max_cdt, add_constant_detour_time=self.add_cdt, min_detour_time_window=self.min_dtw, boarding_time=self.const_bt) @@ -107,6 +113,13 @@ def user_request(self, rq, sim_time): rid_struct = rq.get_rid_struct() self.rq_dict[rid_struct] = prq + parent_rid: int = rq.get_rid() + # get excluded vids for flm request + if rid_struct == f"{parent_rid}_{RQ_SUB_TRIP_ID.FLM_AMOD_1.value}": + excluded_vid: list[int] = self.flm_excluded_vid.get(parent_rid, []) + else: + excluded_vid = [] + if prq.o_pos == prq.d_pos: LOG.debug(f"automatic decline for rid {rid_struct}!") self._create_rejection(prq, sim_time) @@ -115,14 +128,23 @@ def user_request(self, rq, sim_time): o_pos, t_pu_earliest, t_pu_latest = prq.get_o_stop_info() if t_pu_earliest - sim_time > self.opt_horizon: self.reservation_module.add_reservation_request(prq, sim_time) - offer = self.reservation_module.return_immediate_reservation_offer(prq.get_rid_struct(), sim_time) + offer = self.reservation_module.return_immediate_reservation_offer(prq.get_rid_struct(), sim_time, excluded_vid=excluded_vid) LOG.debug(f"reservation offer for rid {rid_struct} : {offer}") else: - list_tuples = insertion_with_heuristics(sim_time, prq, self, force_feasible_assignment=True) + list_tuples = insertion_with_heuristics(sim_time, prq, self, force_feasible_assignment=True, excluded_vid=excluded_vid) if len(list_tuples) > 0: (vid, vehplan, delta_cfv) = min(list_tuples, key=lambda x:x[2]) self.tmp_assignment[rid_struct] = vehplan offer = self._create_user_offer(prq, sim_time, vehplan) + + if rid_struct == f"{parent_rid}_{RQ_SUB_TRIP_ID.FLM_AMOD_0.value}": + assigned_vid: int = vehplan.vid + if parent_rid in self.flm_excluded_vid: + self.flm_excluded_vid[parent_rid].append(assigned_vid) + else: + self.flm_excluded_vid[parent_rid] = [assigned_vid] + LOG.debug(f"FLM: assigned vid {assigned_vid} to fisrt mile amod sub-request {rid_struct}, excluding it for the last mile sub-request {parent_rid}_{RQ_SUB_TRIP_ID.FLM_AMOD_1.value}") + LOG.debug(f"new offer for rid {rid_struct} : {offer}") else: LOG.debug(f"rejection for rid {rid_struct}") diff --git a/src/fleetctrl/RPPFleetControl.py b/src/fleetctrl/RPPFleetControl.py index 375a2486..3e46de85 100644 --- a/src/fleetctrl/RPPFleetControl.py +++ b/src/fleetctrl/RPPFleetControl.py @@ -21,7 +21,7 @@ from src.simulation.Offers import TravellerOffer if TYPE_CHECKING: from src.demand.TravelerModels import RequestBase, ParcelRequestBase - from src.routing.NetworkBase import NetworkBase + from src.routing.road.NetworkBase import NetworkBase from src.simulation.Vehicles import SimulationVehicle # -------------------------------------------------------------------------------------------------------------------- # diff --git a/src/fleetctrl/RidePoolingBatchOptimizationFleetControlBase.py b/src/fleetctrl/RidePoolingBatchOptimizationFleetControlBase.py index 22bdd3bd..c88c5a6f 100644 --- a/src/fleetctrl/RidePoolingBatchOptimizationFleetControlBase.py +++ b/src/fleetctrl/RidePoolingBatchOptimizationFleetControlBase.py @@ -14,7 +14,7 @@ from src.misc.globals import * if TYPE_CHECKING: - from src.routing.NetworkBase import NetworkBase + from src.routing.road.NetworkBase import NetworkBase from src.fleetctrl.pooling.batch.BatchAssignmentAlgorithmBase import BatchAssignmentAlgorithmBase from src.simulation.Vehicles import SimulationVehicle from src.infra.ChargingInfrastructure import OperatorChargingAndDepotInfrastructure, PublicChargingInfrastructureOperator diff --git a/src/fleetctrl/planning/PlanRequest.py b/src/fleetctrl/planning/PlanRequest.py index 1595b443..2e253f0e 100644 --- a/src/fleetctrl/planning/PlanRequest.py +++ b/src/fleetctrl/planning/PlanRequest.py @@ -9,7 +9,7 @@ # ---------------- from src.misc.globals import * from src.demand.TravelerModels import RequestBase -from src.routing.NetworkBase import NetworkBase +from src.routing.road.NetworkBase import NetworkBase from src.simulation.Offers import TravellerOffer LOG = logging.getLogger(__name__) @@ -207,6 +207,11 @@ def set_new_pickup_time_constraint(self, new_latest_pu_time : int, new_earliest_ self.t_pu_earliest = new_earliest_pu_time # LOG.debug("after: {}".format(self)) + def set_new_dropoff_time_constraint(self, new_latest_do_time : int): + """ this function is used to update dropoff time constraints of the plan request + :param new_latest_do_time: new latest dropoff time""" + self.t_do_latest = new_latest_do_time + def set_new_max_trip_time(self, new_max_trip_time : float): """ this function updates the maximum trip time constraint :param new_max_trip_time: new maximum trip time""" diff --git a/src/fleetctrl/planning/VehiclePlan.py b/src/fleetctrl/planning/VehiclePlan.py index 0608bad1..9851f0c1 100644 --- a/src/fleetctrl/planning/VehiclePlan.py +++ b/src/fleetctrl/planning/VehiclePlan.py @@ -14,7 +14,7 @@ from src.simulation.Legs import VehicleRouteLeg from src.simulation.Vehicles import SimulationVehicle from src.fleetctrl.planning.PlanRequest import PlanRequest -from src.routing.NetworkBase import NetworkBase +from src.routing.road.NetworkBase import NetworkBase # -------------------------------------------------------------------------------------------------------------------- # # global variables diff --git a/src/fleetctrl/pooling/GeneralPoolingFunctions.py b/src/fleetctrl/pooling/GeneralPoolingFunctions.py index 44ea1886..26ab7bcc 100644 --- a/src/fleetctrl/pooling/GeneralPoolingFunctions.py +++ b/src/fleetctrl/pooling/GeneralPoolingFunctions.py @@ -2,7 +2,7 @@ from typing import Callable, Dict, List, Any, Tuple, TYPE_CHECKING if TYPE_CHECKING: - from src.routing.NetworkBase import NetworkBase + from src.routing.road.NetworkBase import NetworkBase from src.fleetctrl.pooling.batch.AlonsoMora.AlonsoMoraParallelization import ParallelizationManager from src.fleetctrl.FleetControlBase import FleetControlBase from src.fleetctrl.planning.PlanRequest import PlanRequest diff --git a/src/fleetctrl/pooling/batch/AlonsoMora/AlonsoMoraAssignment.py b/src/fleetctrl/pooling/batch/AlonsoMora/AlonsoMoraAssignment.py index fc7aa128..ea4ff274 100644 --- a/src/fleetctrl/pooling/batch/AlonsoMora/AlonsoMoraAssignment.py +++ b/src/fleetctrl/pooling/batch/AlonsoMora/AlonsoMoraAssignment.py @@ -17,7 +17,7 @@ from src.misc.globals import * from src.simulation.Legs import VehicleRouteLeg if TYPE_CHECKING: - from src.routing.NetworkBase import NetworkBase + from src.routing.road.NetworkBase import NetworkBase from src.fleetctrl.pooling.batch.AlonsoMora.AlonsoMoraParallelization import ParallelizationManager from src.fleetctrl.FleetControlBase import FleetControlBase from src.fleetctrl.planning.PlanRequest import PlanRequest diff --git a/src/fleetctrl/pooling/batch/AlonsoMora/V2RB.py b/src/fleetctrl/pooling/batch/AlonsoMora/V2RB.py index 0eb2c06d..cce98248 100644 --- a/src/fleetctrl/pooling/batch/AlonsoMora/V2RB.py +++ b/src/fleetctrl/pooling/batch/AlonsoMora/V2RB.py @@ -7,7 +7,7 @@ if TYPE_CHECKING: from src.fleetctrl.pooling.batch.BatchAssignmentAlgorithmBase import SimulationVehicleStruct - from src.routing.NetworkBase import NetworkBase + from src.routing.road.NetworkBase import NetworkBase from src.fleetctrl.planning.PlanRequest import PlanRequest from src.simulation.Legs import VehicleRouteLeg diff --git a/src/fleetctrl/pooling/batch/BatchAssignmentAlgorithmBase.py b/src/fleetctrl/pooling/batch/BatchAssignmentAlgorithmBase.py index 7c89ea9a..51aba354 100644 --- a/src/fleetctrl/pooling/batch/BatchAssignmentAlgorithmBase.py +++ b/src/fleetctrl/pooling/batch/BatchAssignmentAlgorithmBase.py @@ -8,7 +8,7 @@ from src.fleetctrl.planning.VehiclePlan import VehiclePlan from src.simulation.Legs import VehicleRouteLeg from src.simulation.Vehicles import SimulationVehicle -from src.routing.NetworkBase import NetworkBase +from src.routing.road.NetworkBase import NetworkBase from src.misc.globals import * LOG = logging.getLogger(__name__) diff --git a/src/fleetctrl/pooling/batch/Simonetto/SimonettoAssignment.py b/src/fleetctrl/pooling/batch/Simonetto/SimonettoAssignment.py index 5705a251..3b12eccd 100644 --- a/src/fleetctrl/pooling/batch/Simonetto/SimonettoAssignment.py +++ b/src/fleetctrl/pooling/batch/Simonetto/SimonettoAssignment.py @@ -11,7 +11,7 @@ from src.fleetctrl.planning.VehiclePlan import VehiclePlan, BoardingPlanStop from src.fleetctrl.pooling.immediate.insertion import simple_remove, insert_prq_in_selected_veh_list from src.misc.globals import * -from src.routing.NetworkBase import NetworkBase +from src.routing.road.NetworkBase import NetworkBase from src.simulation.Legs import VehicleRouteLeg from src.simulation.Vehicles import SimulationVehicle diff --git a/src/fleetctrl/pooling/immediate/insertion.py b/src/fleetctrl/pooling/immediate/insertion.py index 46f4ee6c..fccf310f 100644 --- a/src/fleetctrl/pooling/immediate/insertion.py +++ b/src/fleetctrl/pooling/immediate/insertion.py @@ -2,7 +2,7 @@ from src.fleetctrl.planning.VehiclePlan import BoardingPlanStop, PlanStop, VehiclePlan from src.fleetctrl.planning.PlanRequest import PlanRequest from src.simulation.Vehicles import SimulationVehicle -from src.routing.NetworkBase import NetworkBase +from src.routing.road.NetworkBase import NetworkBase from src.fleetctrl.pooling.immediate.searchVehicles import veh_search_for_immediate_request,\ veh_search_for_reservation_request from src.fleetctrl.pooling.immediate.SelectRV import filter_directionality, filter_least_number_tasks @@ -387,7 +387,13 @@ def single_insertion(veh_obj_list : List[SimulationVehicle], current_vid_to_vehp return current_best_vid, current_best_plan, current_best_obj_delta -def insertion_with_heuristics(sim_time : int, prq : PlanRequest, fleetctrl : FleetControlBase, force_feasible_assignment : bool=True) -> List[Tuple[Any, VehiclePlan, float]]: +def insertion_with_heuristics( + sim_time : int, + prq : PlanRequest, + fleetctrl : FleetControlBase, + force_feasible_assignment : bool = True, + excluded_vid = [] +) -> List[Tuple[Any, VehiclePlan, float]]: """This function searches for suitable vehicles and return vehicle plans with insertions. Different heuristics depending on whether it is an immediate or reservation request can be triggered. See the respective functions for more details. @@ -395,13 +401,14 @@ def insertion_with_heuristics(sim_time : int, prq : PlanRequest, fleetctrl : Fle :param prq: PlanRequest to insert :param fleetctrl: FleetControl instance :param force_feasible_assignment: if True, a feasible solution is assigned even with positive control function value + :param excluded_vid: list of vehicle ids that should not be considered for assignment :return: list of (vid, vehplan, delta_cfv) tuples :rtype: list """ if prq.get_reservation_flag(): - return reservation_insertion_with_heuristics(sim_time, prq, fleetctrl, force_feasible_assignment) + return reservation_insertion_with_heuristics(sim_time, prq, fleetctrl, force_feasible_assignment, excluded_vid=excluded_vid) else: - return immediate_insertion_with_heuristics(sim_time, prq, fleetctrl, force_feasible_assignment) + return immediate_insertion_with_heuristics(sim_time, prq, fleetctrl, force_feasible_assignment, excluded_vid=excluded_vid) def immediate_insertion_with_heuristics(sim_time : int, prq : PlanRequest, fleetctrl : FleetControlBase, @@ -491,7 +498,14 @@ def immediate_insertion_with_heuristics(sim_time : int, prq : PlanRequest, fleet return return_rv_tuples -def reservation_insertion_with_heuristics(sim_time : int, prq : PlanRequest, fleetctrl : FleetControlBase, force_feasible_assignment : bool=True, veh_plans : Dict[int, VehiclePlan] = None) -> List[Tuple[Any, VehiclePlan, float]]: +def reservation_insertion_with_heuristics( + sim_time : int, + prq : PlanRequest, + fleetctrl : FleetControlBase, + force_feasible_assignment : bool = True, + veh_plans : Dict[int, VehiclePlan] = None, + excluded_vid: list[int] = [], +) -> List[Tuple[Any, VehiclePlan, float]]: """This function has access to all FleetControl attributes and therefore can trigger different heuristics and is easily extendable if new ideas for heuristics are developed. @@ -521,6 +535,7 @@ def reservation_insertion_with_heuristics(sim_time : int, prq : PlanRequest, fle :param fleetctrl: FleetControl instance :param force_feasible_assignment: if True, a feasible solution is assigned even with positive control function value :param veh_plans: dict vehicle id -> vehicle plan to insert to; if non fleetctrl.veh_plans is used + :param excluded_vid: list of vehicle ids that should not be considered for assignment :return: list of (vid, vehplan, delta_cfv) tuples :rtype: list """ @@ -533,7 +548,7 @@ def reservation_insertion_with_heuristics(sim_time : int, prq : PlanRequest, fle veh_plans_to_insert_to = fleetctrl.veh_plans # 1) pre vehicle-search processes - excluded_vid = [] + # excluded_vid = [] # 2) vehicle-search process dict_veh_to_av_infos = veh_search_for_reservation_request(sim_time, prq, fleetctrl, list_excluded_vid=excluded_vid, veh_plans=veh_plans_to_insert_to) diff --git a/src/fleetctrl/pooling/immediate/singleVehicleDARP.py b/src/fleetctrl/pooling/immediate/singleVehicleDARP.py index f6566546..d443075a 100644 --- a/src/fleetctrl/pooling/immediate/singleVehicleDARP.py +++ b/src/fleetctrl/pooling/immediate/singleVehicleDARP.py @@ -6,7 +6,7 @@ from src.fleetctrl.planning.VehiclePlan import BoardingPlanStop, PlanStop, VehiclePlan from src.fleetctrl.planning.PlanRequest import PlanRequest from src.simulation.Vehicles import SimulationVehicle -from src.routing.NetworkBase import NetworkBase +from src.routing.road.NetworkBase import NetworkBase from src.fleetctrl.pooling.immediate.insertion import simple_insert, simple_remove LOG = logging.getLogger(__name__) diff --git a/src/fleetctrl/pooling/objectives.py b/src/fleetctrl/pooling/objectives.py index e941f620..f93068d9 100644 --- a/src/fleetctrl/pooling/objectives.py +++ b/src/fleetctrl/pooling/objectives.py @@ -8,7 +8,7 @@ from src.fleetctrl.planning.VehiclePlan import VehiclePlan from src.fleetctrl.planning.PlanRequest import PlanRequest from src.simulation.Vehicles import SimulationVehicle - from src.routing.NetworkBase import NetworkBase + from src.routing.road.NetworkBase import NetworkBase LOG = logging.getLogger(__name__) diff --git a/src/fleetctrl/repositioning/FullSamplingRidePoolingRebalancingMultiStage.py b/src/fleetctrl/repositioning/FullSamplingRidePoolingRebalancingMultiStage.py index cb08ef5a..979b9e6f 100644 --- a/src/fleetctrl/repositioning/FullSamplingRidePoolingRebalancingMultiStage.py +++ b/src/fleetctrl/repositioning/FullSamplingRidePoolingRebalancingMultiStage.py @@ -25,7 +25,7 @@ from typing import TYPE_CHECKING, List, Dict, Tuple, Callable, Any if TYPE_CHECKING: from src.fleetctrl.FleetControlBase import FleetControlBase - from src.routing.NetworkBase import NetworkBase + from src.routing.road.NetworkBase import NetworkBase OPT_TIME_LIMIT = 120 WRITE_SOL = True diff --git a/src/fleetctrl/repositioning/FullSamplingRidePoolingRebalancingMultiStageReservation.py b/src/fleetctrl/repositioning/FullSamplingRidePoolingRebalancingMultiStageReservation.py index 209126ca..da918c08 100644 --- a/src/fleetctrl/repositioning/FullSamplingRidePoolingRebalancingMultiStageReservation.py +++ b/src/fleetctrl/repositioning/FullSamplingRidePoolingRebalancingMultiStageReservation.py @@ -25,7 +25,7 @@ from typing import TYPE_CHECKING, List, Dict, Tuple, Callable, Any if TYPE_CHECKING: from src.fleetctrl.FleetControlBase import FleetControlBase - from src.routing.NetworkBase import NetworkBase + from src.routing.road.NetworkBase import NetworkBase OPT_TIME_LIMIT = 120 WRITE_SOL = True diff --git a/src/fleetctrl/reservation/BatchSchedulingRevelationHorizonBase.py b/src/fleetctrl/reservation/BatchSchedulingRevelationHorizonBase.py index caf83423..ef08bf4a 100644 --- a/src/fleetctrl/reservation/BatchSchedulingRevelationHorizonBase.py +++ b/src/fleetctrl/reservation/BatchSchedulingRevelationHorizonBase.py @@ -7,7 +7,7 @@ from src.simulation.Offers import TravellerOffer from src.fleetctrl.FleetControlBase import FleetControlBase from src.fleetctrl.pooling.immediate.insertion import simple_remove -from src.routing.NetworkBase import NetworkBase +from src.routing.road.NetworkBase import NetworkBase from src.fleetctrl.planning.VehiclePlan import PlanStopBase, RoutingTargetPlanStop from src.fleetctrl.planning.PlanRequest import PlanRequest from src.fleetctrl.reservation.RevelationHorizonBase import RevelationHorizonBase diff --git a/src/fleetctrl/reservation/ContinuousBatchRevelationReservation.py b/src/fleetctrl/reservation/ContinuousBatchRevelationReservation.py index 818efd75..5f9a8bdd 100644 --- a/src/fleetctrl/reservation/ContinuousBatchRevelationReservation.py +++ b/src/fleetctrl/reservation/ContinuousBatchRevelationReservation.py @@ -19,7 +19,7 @@ if TYPE_CHECKING: from src.fleetctrl.FleetControlBase import FleetControlBase - from src.routing.NetworkBase import NetworkBase + from src.routing.road.NetworkBase import NetworkBase import logging LOG = logging.getLogger(__name__) diff --git a/src/fleetctrl/reservation/ReservationRequestBatch.py b/src/fleetctrl/reservation/ReservationRequestBatch.py index 866c91bc..60abb476 100644 --- a/src/fleetctrl/reservation/ReservationRequestBatch.py +++ b/src/fleetctrl/reservation/ReservationRequestBatch.py @@ -1,7 +1,7 @@ import numpy as np import pandas as pd from src.fleetctrl.FleetControlBase import FleetControlBase -from src.routing.NetworkBase import NetworkBase +from src.routing.road.NetworkBase import NetworkBase from src.fleetctrl.planning.VehiclePlan import PlanStopBase from src.fleetctrl.planning.PlanRequest import PlanRequest from src.fleetctrl.reservation.misc.RequestGroup import RequestGroup, QuasiVehiclePlan, VehiclePlanSupportingPoint, rg_key diff --git a/src/fleetctrl/reservation/RollingHorizon.py b/src/fleetctrl/reservation/RollingHorizon.py index f2db16e7..3dd0125a 100644 --- a/src/fleetctrl/reservation/RollingHorizon.py +++ b/src/fleetctrl/reservation/RollingHorizon.py @@ -65,14 +65,15 @@ def return_availability_constraints(self, sim_time): :return: list of (position, latest arrival time)""" return [] - def return_immediate_reservation_offer(self, rid, sim_time): + def return_immediate_reservation_offer(self, rid, sim_time, excluded_vid=[]): """ this function returns an offer if possible for an reservation request which has been added to the reservation module before in this implementation, an offer is always returned discribed by the earliest and latest pick up time :param rid: request id :param sim_time: current simulation time + :param excluded_vid: list of vehicle ids that should not be considered for assignment :return: offer for request """ prq = self.active_reservation_requests[rid] - tuple_list = reservation_insertion_with_heuristics(sim_time, prq, self.fleetctrl, force_feasible_assignment=True) + tuple_list = reservation_insertion_with_heuristics(sim_time, prq, self.fleetctrl, force_feasible_assignment=True, excluded_vid=excluded_vid) if len(tuple_list) > 0: best_tuple = min(tuple_list, key=lambda x:x[2]) best_vid, best_plan, _ = best_tuple @@ -100,15 +101,24 @@ def user_cancels_request(self, rid, simulation_time): :param rid: request id :param simulation_time: current simulation time """ + # If assigned to a vehicle, remove from vehicle plan if self.rid_to_assigned_vid.get(rid) is not None: - vid = self.rid_to_assigned_vid.get[rid] + vid = self.rid_to_assigned_vid[rid] assigned_plan = self.fleetctrl.veh_plans[vid] - veh_obj = self.fleetctrl.veh_plans[vid] + veh_obj = self.fleetctrl.sim_vehicles[vid] new_plan = simple_remove(veh_obj, assigned_plan, rid, simulation_time, self.routing_engine, self.fleetctrl.vr_ctrl_f, self.fleetctrl.rq_dict, self.fleetctrl.const_bt, self.fleetctrl.add_bt) self.fleetctrl.assign_vehicle_plan(veh_obj, new_plan, simulation_time) del self.rid_to_assigned_vid[rid] + + # FIX: Unconditionally clean up active_reservation_requests and sorted_rids_with_epa + # This fixes a bug discovered in PTBrokerEI where cancellation after user_confirms_booking + # (e.g., LM AMoD sub-request cancelled due to uncatchable PT) left stale entries in + # sorted_rids_with_epa, causing KeyError in reveal_requests_for_online_optimization. + # Reference: RollingHorizonNoGuarantee has the correct implementation. + if rid in self.active_reservation_requests: del self.active_reservation_requests[rid] + self.sorted_rids_with_epa = [(r, epa) for r, epa in self.sorted_rids_with_epa if r != rid] def time_trigger(self, sim_time): """ this function is triggered during the simulation time and might trigger reoptimization processes for example diff --git a/src/fleetctrl/reservation/misc/ArtificialReservationFleetControl.py b/src/fleetctrl/reservation/misc/ArtificialReservationFleetControl.py index f69c422f..a3e74e61 100644 --- a/src/fleetctrl/reservation/misc/ArtificialReservationFleetControl.py +++ b/src/fleetctrl/reservation/misc/ArtificialReservationFleetControl.py @@ -1,7 +1,7 @@ from src.fleetctrl.RidePoolingBatchAssignmentFleetcontrol import RidePoolingBatchAssignmentFleetcontrol from src.fleetctrl.planning.VehiclePlan import VehiclePlan, BoardingPlanStop, PlanStopBase, RoutingTargetPlanStop from src.fleetctrl.planning.PlanRequest import PlanRequest -from src.routing.NetworkBase import NetworkBase +from src.routing.road.NetworkBase import NetworkBase from src.fleetctrl.pooling.immediate.insertion import simple_insert, simple_remove from src.misc.globals import * from typing import Any, Callable, List, Dict, Tuple, Type diff --git a/src/fleetctrl/reservation/misc/RequestGroup.py b/src/fleetctrl/reservation/misc/RequestGroup.py index f8419b54..be8ccb12 100644 --- a/src/fleetctrl/reservation/misc/RequestGroup.py +++ b/src/fleetctrl/reservation/misc/RequestGroup.py @@ -1,7 +1,7 @@ from src.fleetctrl.FleetControlBase import FleetControlBase from src.fleetctrl.planning.VehiclePlan import VehiclePlan, BoardingPlanStop, PlanStopBase from src.fleetctrl.planning.PlanRequest import PlanRequest -from src.routing.NetworkBase import NetworkBase +from src.routing.road.NetworkBase import NetworkBase from src.fleetctrl.pooling.immediate.insertion import simple_insert, simple_remove from src.misc.globals import * from typing import Any, Callable, List, Dict, Tuple, Type diff --git a/src/fleetctrl/rideparcelpooling/immediate/insertion.py b/src/fleetctrl/rideparcelpooling/immediate/insertion.py index 0b6a9acf..f38b91e3 100644 --- a/src/fleetctrl/rideparcelpooling/immediate/insertion.py +++ b/src/fleetctrl/rideparcelpooling/immediate/insertion.py @@ -4,7 +4,7 @@ from src.fleetctrl.planning.VehiclePlan import BoardingPlanStop, VehiclePlan from src.fleetctrl.planning.PlanRequest import PlanRequest from src.simulation.Vehicles import SimulationVehicle -from src.routing.NetworkBase import NetworkBase +from src.routing.road.NetworkBase import NetworkBase from src.fleetctrl.pooling.immediate.insertion import simple_insert from src.misc.globals import * import numpy as np diff --git a/src/infra/ChargingInfrastructure.py b/src/infra/ChargingInfrastructure.py index f56e18a1..8d1f41f5 100644 --- a/src/infra/ChargingInfrastructure.py +++ b/src/infra/ChargingInfrastructure.py @@ -26,7 +26,7 @@ from src.fleetctrl.planning.VehiclePlan import ChargingPlanStop, VehiclePlan, RoutingTargetPlanStop from src.misc.config import decode_config_str if TYPE_CHECKING: - from src.routing.NetworkBase import NetworkBase + from src.routing.road.NetworkBase import NetworkBase from src.simulation.Vehicles import SimulationVehicle from src.fleetctrl.FleetControlBase import FleetControlBase diff --git a/src/misc/globals.py b/src/misc/globals.py index 7217373d..38457c61 100644 --- a/src/misc/globals.py +++ b/src/misc/globals.py @@ -73,6 +73,9 @@ # broker specific attributes G_BROKER_TYPE = "broker_type" +G_BROKER_TRANSFER_SEARCH_METHOD = "broker_transfer_search_method" # method for finding transfer stations: "closest" or "best_overall" +G_BROKER_MAAS_DETOUR_TIME_FACTOR = "broker_maas_detour_time_factor" # factor to estimate detour time for MaaS intermodal trips (default: 1.0) +G_BROKER_ALWAYS_QUERY_PT = "broker_always_query_pt" # if true, pure PT offers are always queried as a backup option # public transport specific attributes G_PT_TYPE = "pt_type" @@ -105,6 +108,9 @@ G_PT_ROUTE_ID = "pt_route_id" G_PT_WALK_LOGIT_BETA = "pt_walk_logit_beta" G_PT_X_TOL = 0.01 +G_PT_SIM_START_DATE = "pt_simulation_start_date" # data string in format YYYYMMDD; this is a mandatory parameter for the Raptor Router +G_PT_OPERATOR_ID = "pt_operator_id" # id of the public transport operator +G_PT_OPERATOR_TYPE = "pt_operator_type" # type of the public transport operator # zonal control reward attributes G_PT_ZC_RID_SIM_TIME = 0 @@ -147,6 +153,7 @@ G_IM_MIN_MOD_DISTANCE = "min_IM_MOD_distance" G_IM_PER_KM_SUBSIDY = "subsidy_IM_MOD_per_km" G_IM_TRANSFER_TIME = "im_transfer_time" +G_IM_LM_WAIT_TIME = "im_lastmile_wait_time" # customizable wait time for last mile AMoD pickup # operator general attributes @@ -359,7 +366,8 @@ G_DIR_DEMAND = "demand" G_DIR_ZONES = "zones" G_DIR_FC = "forecasts" -G_DIR_PT = "pubtrans" +G_DIR_PT = "pt" +G_DIR_GTFS = "gtfs" G_DIR_VEH = "vehicles" G_DIR_FCTRL = "fleetctrl" G_DIR_BP = "boardingpoints" @@ -470,6 +478,61 @@ G_RQ_PA_EDT = "parcel_earliest_dropoff_time" G_RQ_PA_LDT = "parcel_latest_dropoff_time" +# intermodal specific +G_RQ_RID_STRUCT = "rid_struct" +G_RQ_IS_PARENT_REQUEST = "is_parent_request" +G_RQ_MODAL_STATE = "modal_state" +G_RQ_MODAL_STATE_VALUE = "modal_state_value" +G_RQ_TRANSFER_STATION_IDS = "transfer_station_ids" +G_RQ_MAX_TRANSFERS = "max_transfers" +G_RQ_SUB_TRIP_ID = "sub_trip_id" +G_RQ_UNCATCHABLE_PT = "uncatchable_pt" # flag for requests that missed their PT connection after FM leg +# PAYG (Plan-As-You-Go) specific +G_RQ_PAYG_INTERRUPTED = "payg_interrupted" # flag for PAYG trips that were interrupted +G_RQ_PAYG_INTERRUPT_STATE = "payg_interrupt_state" # PAYG_TRIP_STATE value when interrupted +G_RQ_PAYG_INTERRUPT_TIME = "payg_interrupt_time" # simulation time when interrupted + +class RQ_MODAL_STATE(Enum): + """ This enum is used to identify different modal states of a traveler request. + MONOMODAL: only amod is used + FIRSTMILE: amod first mile and pt last mile + LASTMILE: amod last mile and pt first mile + FIRSTLASTMILE: amod first and last miles, pt in between + PT: only pt is used + ALL_OPTIONS: all options are used + """ + MONOMODAL: int = 0 + FIRSTMILE: int = 1 + LASTMILE: int = 2 + FIRSTLASTMILE: int = 3 + PT: int = 4 + ALL_OPTIONS: int = 5 + +class RQ_SUB_TRIP_ID(Enum): + """ This enum is used to identify different sub-trip ids of a traveler request. + """ + AMOD: int = 0 + FM_AMOD: int = 1 + FM_PT: int = 2 + LM_PT: int = 3 + LM_AMOD: int = 4 + FLM_AMOD_0: int = 5 + FLM_PT: int = 6 + FLM_AMOD_1: int = 7 + PT: int = 8 + +class PAYG_TRIP_STATE(Enum): + """PAYG trip state tracking""" + PENDING = 0 # Waiting for processing + FM_AMOD_BOOKED = 1 # FM/FLM: First AMoD leg booked + FM_AMOD_COMPLETED = 2 # FM/FLM: First AMoD leg completed + PT_BOOKED = 3 # PT leg booked + PT_COMPLETED = 4 # PT leg completed + LM_AMOD_BOOKED = 5 # LM/FLM: Last AMoD leg booked + COMPLETED = 10 # Trip completed + INTERRUPTED_NO_PT = -1 # Interrupted: No PT available + INTERRUPTED_NO_LM_AMOD = -2 # Interrupted: No LM AMoD available + # output general # -------------- G_RQ_TYPE = "rq_type" @@ -485,7 +548,6 @@ G_RQ_FARE = "fare" G_RQ_ACCESS = "access_time" G_RQ_EGRESS = "egress_time" -G_RQ_MODAL_STATE = "modal_state" # (see traveler modal state -> indicates monomodal/intermodal) # output environment specific # --------------------------- @@ -507,12 +569,6 @@ G_RQ_DEC_WT_FAC = "waiting_time_factor" G_RQ_DEC_REAC = "reaction_time" -# traveler modal state -G_RQ_STATE_MONOMODAL = 0 -G_RQ_STATE_FIRSTMILE = 1 -G_RQ_STATE_LASTMILE = 2 -G_RQ_STATE_FIRSTLASTMILE = 3 - # -------------------------------------------------------------------------------------------------------------------- # # Mode Choice Model # ----------------- @@ -562,6 +618,35 @@ G_IM_OFFER_MOD_COST = "im_mod_fare" G_IM_OFFER_MOD_SUB = "im_mod_subsidy" +G_IM_OFFER_OPERATOR_SUB_TRIP_TUPLE = "im_operator_sub_trip_tuple" # tuple of operator ids for each sub-trip: ((operator_id, sub_trip_id),) +G_IM_OFFER_FLM_WAIT_0 = "im_t_wait_flm_0" # Only used for FM AMoD segment in FLM +G_IM_OFFER_FLM_WAIT_1 = "im_t_wait_flm_1" # Only used for LM AMoD segment in FLM +G_IM_OFFER_FLM_DRIVE_0 = "im_t_drive_flm_0" # Only used for FM AMoD segment in FLM +G_IM_OFFER_FLM_DRIVE_1 = "im_t_drive_flm_1" # Only used for LM AMoD segment in FLM +G_IM_OFFER_FM_WAIT = "im_t_wait_fm" # Only used for AMoD segment in FM +G_IM_OFFER_LM_WAIT = "im_t_wait_lm" # Only used fot AMoD segment in LM +G_IM_OFFER_FM_DRIVE = "im_t_drive_fm" # Only used for AMoD segment in FM +G_IM_OFFER_LM_DRIVE = "im_t_drive_lm" # Only used fot AMoD segment in LM +G_IM_OFFER_PT_WAIT = "im_t_wait_pt" # total pt waiting time +G_IM_OFFER_PT_DRIVE = "im_t_drive_pt" # total pt driving time +G_IM_OFFER_DURATION = "im_t_duration" # total duration of intermodal offer + +# additional parameters for pt offers +# ------------------------------------------ +G_PT_OFFER_SOURCE_STATION = "source_station_id" +G_PT_OFFER_TARGET_STATION = "target_station_id" +G_PT_OFFER_SOURCE_WALKING_TIME = "source_walking_time" +G_PT_OFFER_SOURCE_STATION_DEPARTURE_TIME = "source_station_departure_time" +G_PT_OFFER_SOURCE_TRANSFER_TIME = "source_transfer_time" +G_PT_OFFER_SOURCE_WAITING_TIME = "source_waiting_time" +G_PT_OFFER_TRIP_TIME = "trip_time" +G_PT_OFFER_TARGET_TRANSFER_TIME = "target_transfer_time" +G_PT_OFFER_TARGET_STATION_ARRIVAL_TIME = "target_station_arrival_time" +G_PT_OFFER_TARGET_WALKING_TIME = "target_walking_time" +G_PT_OFFER_NUM_TRANSFERS = "num_transfers" +G_PT_OFFER_STEPS = "steps" +G_PT_OFFER_DURATION = "duration" # PT segment total duration + # -------------------------------------------------------------------------------------------------------------------- # # Fleet Simulation Pattern # ######################## @@ -693,6 +778,8 @@ class G_PLANSTOP_STATES(Enum): #--------------------------------------------------------------------------------------------------------------# # Evaluation specific params # #################### +# which evaluation method to use +G_EVAL_METHOD = "evaluation_method" # only evaluate data within specific interval G_EVAL_INT_START = "evaluation_int_start" @@ -748,7 +835,8 @@ def get_directory_dict(scenario_parameters, list_operator_dicts, abs_fleetpy_dir if zone_name is not None: dirs[G_DIR_ZONES] = os.path.join(dirs[G_DIR_DATA], "zones", zone_name, network_name) if gtfs_name is not None: - dirs[G_DIR_PT] = os.path.join(dirs[G_DIR_DATA], "pubtrans", gtfs_name) + dirs[G_DIR_PT] = os.path.join(dirs[G_DIR_DATA], "pt", gtfs_name) + dirs[G_DIR_GTFS] = os.path.join(dirs[G_DIR_DATA], "pt", network_name, gtfs_name, "matched") if infra_name is not None: dirs[G_DIR_INFRA] = os.path.join(dirs[G_DIR_DATA], "infra", infra_name, network_name) if parcel_demand_name is not None: diff --git a/src/misc/init_modules.py b/src/misc/init_modules.py index 46245ded..1b945c56 100644 --- a/src/misc/init_modules.py +++ b/src/misc/init_modules.py @@ -5,9 +5,10 @@ import typing as tp if tp.TYPE_CHECKING: from src.FleetSimulationBase import FleetSimulationBase - from src.routing.NetworkBase import NetworkBase + from src.routing.road.NetworkBase import NetworkBase from src.fleetctrl.FleetControlBase import FleetControlBase from src.broker.BrokerBase import BrokerBase + from src.ptctrl.PTControlBase import PTControlBase from src.demand.TravelerModels import RequestBase from src.fleetctrl.repositioning.RepositioningBase import RepositioningBase from src.fleetctrl.charging.ChargingBase import ChargingBase @@ -57,14 +58,14 @@ def get_src_simulation_environments(): def get_src_routing_engines(): # FleetPy routing engine options re_dict = {} # str -> (module path, class name) - re_dict["NetworkBasic"] = ("src.routing.NetworkBasic", "NetworkBasic") - re_dict["NetworkImmediatePreproc"] = ("src.routing.NetworkImmediatePreproc", "NetworkImmediatePreproc") - re_dict["NetworkBasicWithStore"] = ("src.routing.NetworkBasicWithStore", "NetworkBasicWithStore") - re_dict["NetworkPartialPreprocessed"] = ("src.routing.NetworkPartialPreprocessed", "NetworkPartialPreprocessed") - re_dict["NetworkBasicWithStoreCpp"] = ("src.routing.NetworkBasicWithStoreCpp", "NetworkBasicWithStoreCpp") - re_dict["NetworkBasicCpp"] = ("src.routing.NetworkBasicCpp", "NetworkBasicCpp") - re_dict["NetworkPartialPreprocessedCpp"] = ("src.routing.NetworkPartialPreprocessedCpp", "NetworkPartialPreprocessedCpp") - re_dict["NetworkTTMatrix"] = ("src.routing.NetworkTTMatrix", "NetworkTTMatrix") + re_dict["NetworkBasic"] = ("src.routing.road.NetworkBasic", "NetworkBasic") + re_dict["NetworkImmediatePreproc"] = ("src.routing.road.NetworkImmediatePreproc", "NetworkImmediatePreproc") + re_dict["NetworkBasicWithStore"] = ("src.routing.road.NetworkBasicWithStore", "NetworkBasicWithStore") + re_dict["NetworkPartialPreprocessed"] = ("src.routing.road.NetworkPartialPreprocessed", "NetworkPartialPreprocessed") + re_dict["NetworkBasicWithStoreCpp"] = ("src.routing.road.NetworkBasicWithStoreCpp", "NetworkBasicWithStoreCpp") + re_dict["NetworkBasicCpp"] = ("src.routing.road.NetworkBasicCpp", "NetworkBasicCpp") + re_dict["NetworkPartialPreprocessedCpp"] = ("src.routing.road.NetworkPartialPreprocessedCpp", "NetworkPartialPreprocessedCpp") + re_dict["NetworkTTMatrix"] = ("src.routing.road.NetworkTTMatrix", "NetworkTTMatrix") # add development content if dev_content is not None: dev_re_dict = dev_content.add_dev_routing_engines() @@ -87,6 +88,7 @@ def get_src_request_modules(): rm_dict["BrokerDecisionRequest"] = ("src.demand.TravelerModels", "BrokerDecisionRequest") rm_dict["UserDecisionRequest"] = ("src.demand.TravelerModels", "UserDecisionRequest") rm_dict["PreferredOperatorRequest"] = ("src.demand.TravelerModels", "PreferredOperatorRequest") + rm_dict["BasicIntermodalRequest"] = ("src.demand.TravelerModels", "BasicIntermodalRequest") # add development content if dev_content is not None: dev_rm_dict = dev_content.add_request_models() @@ -116,12 +118,25 @@ def get_src_broker_modules(): # FleetPy broker options broker_dict = {} # str -> (module path, class name) broker_dict["BrokerBasic"] = ("src.broker.BrokerBasic", "BrokerBasic") + broker_dict["PTBroker"] = ("src.broker.PTBroker", "PTBroker") + broker_dict["PTBrokerEI"] = ("src.broker.PTBrokerEI", "PTBrokerEI") + broker_dict["PTBrokerPAYG"] = ("src.broker.PTBrokerPAYG", "PTBrokerPAYG") # add development content if dev_content is not None: dev_broker_dict = dev_content.add_broker_modules() broker_dict.update(dev_broker_dict) return broker_dict +def get_src_pt_control_modules(): + # FleetPy pt control options + ptc_dict = {} # str -> (module path, class name) + ptc_dict["PTControlBasic"] = ("src.ptctrl.PTControlBasic", "PTControlBasic") + # add development content + if dev_content is not None: + dev_ptc_dict = dev_content.add_pt_control_modules() + ptc_dict.update(dev_ptc_dict) + return ptc_dict + def get_src_repositioning_strategies(): repo_dict = {} # str -> (module path, class name) repo_dict["PavoneFC"] = ("src.fleetctrl.repositioning.PavoneHailingFC", "PavoneHailingRepositioningFC") @@ -284,6 +299,18 @@ def load_broker_module(broker_type) -> BrokerBase: # get broker class return load_module(broker_dict, broker_type, "Broker module") +def load_pt_control_module(pt_control_type) -> PTControlBase: + """This function initiates the required pt control module and returns the PTControl class, which can be used + to generate a pt operator instance. + + :param pt_control_type: string that determines which pt control should be used + :return: PTControl class + """ + # FleetPy pt control options + ptc_dict = get_src_pt_control_modules() + # get pt control class + return load_module(ptc_dict, pt_control_type, "PT control module") + def load_repositioning_strategy(op_repo_class_string) -> RepositioningBase: """This function chooses the repositioning module that should be loaded. diff --git a/src/preprocessing/demand/aggregate_trip_demand.py b/src/preprocessing/demand/aggregate_trip_demand.py index d904f3c7..1d2578bd 100644 --- a/src/preprocessing/demand/aggregate_trip_demand.py +++ b/src/preprocessing/demand/aggregate_trip_demand.py @@ -5,7 +5,7 @@ import pandas as pd BASEPATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) sys.path.append(BASEPATH) -from src.routing.NetworkBasicWithStoreCpp import NetworkBasicWithStoreCpp +from src.routing.road.NetworkBasicWithStoreCpp import NetworkBasicWithStoreCpp from src.misc.safe_pathname import slugify from src.misc.globals import * diff --git a/src/preprocessing/infra/access_point_distribution_preprocessing.py b/src/preprocessing/infra/access_point_distribution_preprocessing.py index 45100e01..380ff9d6 100644 --- a/src/preprocessing/infra/access_point_distribution_preprocessing.py +++ b/src/preprocessing/infra/access_point_distribution_preprocessing.py @@ -9,7 +9,7 @@ print(MAIN_DIR) sys.path.append(MAIN_DIR) -from src.routing.NetworkBasic import NetworkBasic as Network +from src.routing.road.NetworkBasic import NetworkBasic as Network def routing_min_distance_cost_function(travel_time, travel_distance, current_node_index): """computes the customized section cost for routing (input for routing functions) diff --git a/src/preprocessing/infra/preprocess_boarding_point_distances.py b/src/preprocessing/infra/preprocess_boarding_point_distances.py index 0dfbb472..9bc19545 100644 --- a/src/preprocessing/infra/preprocess_boarding_point_distances.py +++ b/src/preprocessing/infra/preprocess_boarding_point_distances.py @@ -7,7 +7,7 @@ tum_fleet_sim_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) sys.path.append(tum_fleet_sim_path) -from src.routing.NetworkBasic import NetworkBasic as Network +from src.routing.road.NetworkBasic import NetworkBasic as Network from src.infra.BoardingPointInfrastructure import BoardingPointInfrastructure def routing_min_distance_cost_function(travel_time, travel_distance, current_node_index): diff --git a/src/preprocessing/networks/create_Xto1_preprocessing_files.py b/src/preprocessing/networks/create_Xto1_preprocessing_files.py index faae035f..3e3da8a5 100644 --- a/src/preprocessing/networks/create_Xto1_preprocessing_files.py +++ b/src/preprocessing/networks/create_Xto1_preprocessing_files.py @@ -5,11 +5,11 @@ from multiprocessing import Pool fleet_sim_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))) try: - from src.routing.NetworkBasic import NetworkBasic as Network + from src.routing.road.NetworkBasic import NetworkBasic as Network except: #fleet_sim_path = r'C:\Users\ge37ser\Documents\Coding\TUM_VT_FleetSimulation\tum-vt-fleet-simulation' #to be adopted os.sys.path.append(fleet_sim_path) - from src.routing.NetworkBasic import NetworkBasic as Network + from src.routing.road.NetworkBasic import NetworkBasic as Network """ this script is used to preprocess travel time tables for the routing_engine NetworkPartialPreprocessed.py diff --git a/src/preprocessing/networks/create_partially_preprocessed_travel_time_tables.py b/src/preprocessing/networks/create_partially_preprocessed_travel_time_tables.py index 48b4b808..535ed65c 100644 --- a/src/preprocessing/networks/create_partially_preprocessed_travel_time_tables.py +++ b/src/preprocessing/networks/create_partially_preprocessed_travel_time_tables.py @@ -7,10 +7,10 @@ fleet_sim_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))) os.sys.path.append(fleet_sim_path) try: - from src.routing.NetworkBasicCpp import NetworkBasicCpp as Network + from src.routing.road.NetworkBasicCpp import NetworkBasicCpp as Network except: print("cpp router not found") - from src.routing.NetworkBasic import NetworkBasic as Network + from src.routing.road.NetworkBasic import NetworkBasic as Network """ this script is used to preprocess travel time tables for the routing_engine diff --git a/src/preprocessing/pt/PTRouterGTFSPreperation.ipynb b/src/preprocessing/pt/PTRouterGTFSPreperation.ipynb new file mode 100644 index 00000000..721b79a6 --- /dev/null +++ b/src/preprocessing/pt/PTRouterGTFSPreperation.ipynb @@ -0,0 +1,651 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4ba79989", + "metadata": {}, + "source": [ + "> ### 💡 Info: Data Preparation for FleetPy Raptor Router\n", + "> \n", + "> This notebook guides you through cleaning and formatting raw **GTFS data** into the specific file formats required by **FleetPy's Raptor Router**.\n", + ">\n", + "> #### 📄 Input Specifications:\n", + "> * **Mandatory GTFS Files:** `agency`, `calendar`, `routes`, `stop_times`, `stops`, `transfers`, and `trips`.\n", + "> * **Optional GTFS Files:** `calendar_dates`.\n", + "> * **Custom Required Files:** \n", + "> * `stations` (General requirement)\n", + "> * `street_station_transfers` (Required for the **PTBrokerTPCS** module)\n", + ">\n", + "> **Note:** Any files not mentioned above will be ignored by the FleetPy reader.\n", + ">\n", + "> #### 📝 Output Naming Convention:\n", + "> To distinguish processed files from the originals, the suffix `_fp` will be appended to each filename: \n", + "> `{original_filename}.txt` $\\rightarrow$ `{original_filename}_fp.txt`\n", + ">\n", + "> #### 📂 Output Directory Structure:\n", + "> All processed files must be placed in the following directory path:\n", + ">\n", + "> `data/pt/{network_name}/{gtfs_name}/matched`\n", + ">\n", + "> * **`{network_name}`**: The name of the road network used in your FleetPy experiment.\n", + "> * **`{gtfs_name}`**: A custom identifier (string) that must be defined in your scenario configuration." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "09edb0cb", + "metadata": {}, + "outputs": [], + "source": [ + "# standard library imports\n", + "import pandas as pd\n", + "from pathlib import Path\n", + "import os" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "6cf7b630", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/Users/dch/Projects/FleetPy/ptbroker_to_be_merged/data/pt/example_network/example_gtfs/matched\n" + ] + } + ], + "source": [ + "current_dir = Path.cwd()\n", + "MAIN_DIR = current_dir.parents[2]\n", + "example_gtfs_path = os.path.join(MAIN_DIR, 'data', 'pt', 'example_network', 'example_gtfs', 'matched')\n", + "print(example_gtfs_path)" + ] + }, + { + "cell_type": "markdown", + "id": "03c87c07", + "metadata": {}, + "source": [ + "# Mandatory GTFS Files" + ] + }, + { + "cell_type": "markdown", + "id": "6e4f72a8", + "metadata": {}, + "source": [ + "### agency" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "1ce0d496", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Agency file preview:\n", + " agency_id agency_name\n", + "0 pt FleetPy PT Operator\n" + ] + } + ], + "source": [ + "# read example agency file\n", + "agency_fp = pd.read_csv(os.path.join(example_gtfs_path, 'agency_fp.txt'))\n", + "print(\"\\nAgency file preview:\")\n", + "print(agency_fp.head())\n" + ] + }, + { + "cell_type": "markdown", + "id": "8101895b", + "metadata": {}, + "source": [ + "> #### 📄 Agency File Requirements\n", + "> \n", + "> **Required Columns:**\n", + "> * `agency_id`: Can be of type `int` or `string`.\n", + "> * `agency_name`: Must be of type `string`.\n", + "> \n", + "> ⚠️ **Critical Warning:** > When handling string fields (especially `agency_name`), **strictly avoid** using special characters such as commas (`,`), semicolons (`;`), or colons (`:`). \n", + "> These characters can interfere with file parsing and cause the underlying **C++ backend to crash**." + ] + }, + { + "cell_type": "markdown", + "id": "2baeb223", + "metadata": {}, + "source": [ + "### calendar" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3f9b8f91", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Calendar file preview:\n", + " service_id monday tuesday wednesday thursday friday saturday sunday \\\n", + "0 0 1 1 1 1 1 1 1 \n", + "\n", + " start_date end_date \n", + "0 20000101 20991231 \n" + ] + } + ], + "source": [ + "# read example calendar file\n", + "calendar_fp = pd.read_csv(os.path.join(example_gtfs_path, 'calendar_fp.txt'))\n", + "print(\"\\nCalendar file preview:\")\n", + "print(calendar_fp.head())" + ] + }, + { + "cell_type": "markdown", + "id": "4238ce98", + "metadata": {}, + "source": [ + "> #### 🗓️ Calendar File Specifications\n", + "> \n", + "> **Column Naming:** > Ensure strict adherence to the column naming conventions found in the provided example file.\n", + "> \n", + "> **Data Formatting & Types:**\n", + "> * `start_date` & `end_date`: Must be in **`YYYYMMDD`** format.\n", + "> * `service_id`: Can be of type `int` or `string`.\n", + "> * **All other columns:** Must be of type `int`." + ] + }, + { + "cell_type": "markdown", + "id": "18433a16", + "metadata": {}, + "source": [ + "### routes" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "314dd822", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Routes file preview:\n", + " route_id route_short_name route_desc\n", + "0 196 196 Bus\n", + "1 U5 U5 U-Bahn\n" + ] + } + ], + "source": [ + "# read example routes file\n", + "routes_fp = pd.read_csv(os.path.join(example_gtfs_path, 'routes_fp.txt'))\n", + "print(\"\\nRoutes file preview:\")\n", + "print(routes_fp.head())" + ] + }, + { + "cell_type": "markdown", + "id": "9e2784e6", + "metadata": {}, + "source": [ + "> #### 🚌 Routes File Requirements\n", + "> \n", + "> **Column Specifications:**\n", + "> * **Mandatory:** `route_id`, `route_short_name`.\n", + "> * **Optional:** `route_desc`.\n", + "> * **Data Types:** All the columns above accept either `int` or `string` types.\n", + "> \n", + "> ⚠️ **Critical Warning:** > For all string fields (especially `route_desc` and `route_short_name`), **strictly avoid** using special characters such as commas (`,`), semicolons (`;`), or colons (`:`).\n", + "> Presence of these symbols can interfere with parsing and cause the **C++ backend to crash**." + ] + }, + { + "cell_type": "markdown", + "id": "7ba044a2", + "metadata": {}, + "source": [ + "### stops" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "6bd77ecf", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Stops file preview:\n", + " stop_id\n", + "0 1-0\n", + "1 1-1\n", + "2 1-2\n", + "3 1-3\n", + "4 2-0\n" + ] + } + ], + "source": [ + "# read example stops file\n", + "stops_fp = pd.read_csv(os.path.join(example_gtfs_path, 'stops_fp.txt'))\n", + "print(\"\\nStops file preview:\")\n", + "print(stops_fp.head())" + ] + }, + { + "cell_type": "markdown", + "id": "d3deda00", + "metadata": {}, + "source": [ + "> #### 🚏 Stops File Requirements\n", + "> \n", + "> **Purpose & Optimization:**\n", + "> Since the FleetPy Raptor Router is designed solely to calculate the fastest route between two points, the `stops` file can be minimal.\n", + ">\n", + "> **Required Column:**\n", + "> * **`stop_id`**: This is the only column required to represent and index PT stops.\n", + "> * **Note:** All other columns (e.g., stop names, coordinates) are unnecessary for this specific input file and can be omitted.\n", + "> \n", + "> **Data Type:**\n", + "> * `stop_id` can be of type `int` or `string`.\n", + "> \n", + "> ⚠️ **Critical Warning:** > If `stop_id` is a string, **strictly avoid** using special characters such as commas (`,`), semicolons (`;`), or colons (`:`).\n", + "> These symbols can cause the **C++ backend to crash**." + ] + }, + { + "cell_type": "markdown", + "id": "99410a83", + "metadata": {}, + "source": [ + "### trips" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "8b1dfa77", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Trips file preview:\n", + " route_id trip_id service_id direction_id\n", + "0 196 196-0-0 0 0\n", + "1 196 196-1-0 0 0\n", + "2 196 196-2-0 0 0\n", + "3 196 196-3-0 0 0\n", + "4 196 196-4-0 0 0\n" + ] + } + ], + "source": [ + "# read example trips file\n", + "trips_fp = pd.read_csv(os.path.join(example_gtfs_path, 'trips_fp.txt'))\n", + "print(\"\\nTrips file preview:\")\n", + "print(trips_fp.head())" + ] + }, + { + "cell_type": "markdown", + "id": "9d94b39b", + "metadata": {}, + "source": [ + "> #### 🔄 Trips File Requirements\n", + "> \n", + "> **Mandatory Columns:**\n", + "> * `route_id`: `int` or `string`\n", + "> * `trip_id`: `int` or `string`\n", + "> * `service_id`: `int` or `string`\n", + "> * `direction_id`: `int`\n", + "> \n", + "> ⚠️ **Critical Warning:** > For any string fields, **strictly avoid** using special characters such as commas (`,`), semicolons (`;`), or colons (`:`).\n", + "> These symbols can interfere with parsing and cause the **C++ backend to crash**." + ] + }, + { + "cell_type": "markdown", + "id": "8b369960", + "metadata": {}, + "source": [ + "### stop_times" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "9dd8a151", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Stop_times file preview:\n", + " trip_id arrival_time departure_time stop_id stop_sequence\n", + "0 196-0-0 0:00:00 0:00:00 1-0 1\n", + "1 196-0-0 0:02:00 0:02:00 2-0 2\n", + "2 196-0-0 0:03:30 0:03:30 3-0 3\n", + "3 196-0-0 0:04:30 0:04:30 4-0 4\n", + "4 196-0-0 0:05:30 0:05:30 5-0 5\n" + ] + } + ], + "source": [ + "# read example stop_times file\n", + "stop_times_fp = pd.read_csv(os.path.join(example_gtfs_path, 'stop_times_fp.txt'))\n", + "print(\"\\nStop_times file preview:\")\n", + "print(stop_times_fp.head())" + ] + }, + { + "cell_type": "markdown", + "id": "bd84356b", + "metadata": {}, + "source": [ + "> #### ⏱️ Stop Times File Requirements\n", + "> \n", + "> **Mandatory Columns:**\n", + "> * `trip_id`\n", + "> * `arrival_time`\n", + "> * `departure_time`\n", + "> * `stop_id`\n", + "> * `stop_sequence`\n", + "> \n", + "> **Data Formatting Rules:**\n", + "> * **Time Fields:** `arrival_time` and `departure_time` must be strictly formatted as **`HH:MM:SS`**.\n", + "> * **Sequence:** `stop_sequence` must be an **integer greater than 0**." + ] + }, + { + "cell_type": "markdown", + "id": "9a41f81f", + "metadata": {}, + "source": [ + "### transfers" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "41917216", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Transfers file preview:\n", + " from_stop_id to_stop_id min_transfer_time\n", + "0 1-0 1-1 3\n", + "1 1-0 1-2 3\n", + "2 1-0 1-3 3\n", + "3 1-1 1-0 3\n", + "4 1-1 1-2 3\n" + ] + } + ], + "source": [ + "# read example transfers file\n", + "transfers_fp = pd.read_csv(os.path.join(example_gtfs_path, 'transfers_fp.txt'))\n", + "print(\"\\nTransfers file preview:\")\n", + "print(transfers_fp.head())" + ] + }, + { + "cell_type": "markdown", + "id": "59a8d1c4", + "metadata": {}, + "source": [ + "> #### ⇄ Transfers File Requirements\n", + "> \n", + "> **Mandatory Columns:**\n", + "> * `from_stop_id`\n", + "> * `to_stop_id`\n", + "> * `min_transfer_time`\n", + "> \n", + "> **Data Specifications:**\n", + "> * `min_transfer_time`: Must be of type `int`. This value represents the required transfer duration between stops **in seconds**.\n", + "> \n", + "> **Connectivity Rule:**\n", + "> * If a specific pair of stops is not listed in this file, it implies that **no transfer is possible** between them." + ] + }, + { + "cell_type": "markdown", + "id": "8bacc072", + "metadata": {}, + "source": [ + "# Optional GTFS Files" + ] + }, + { + "cell_type": "markdown", + "id": "b04bce9e", + "metadata": {}, + "source": [ + "### calendar_dates" + ] + }, + { + "cell_type": "markdown", + "id": "685b376a", + "metadata": {}, + "source": [ + "> #### 📅 Calendar Dates File Requirements\n", + "> \n", + "> **Mandatory Columns:**\n", + "> * `service_id`\n", + "> * `date`\n", + "> * `exception_type`\n", + "> \n", + "> **Data Specifications:**\n", + "> * **Date:** The `date` column must be in **`YYYYMMDD`** format.\n", + "> * **Exception Type:** The `exception_type` column accepts only values `1` or `2`:\n", + "> * **`1`**: Service has been **added** for the specified date.\n", + "> * **`2`**: Service has been **removed** for the specified date." + ] + }, + { + "cell_type": "markdown", + "id": "2a4467c4", + "metadata": {}, + "source": [ + "# Custom Required Files" + ] + }, + { + "cell_type": "markdown", + "id": "3250a8fc", + "metadata": {}, + "source": [ + "### stations" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "d5030f8e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Stations file preview:\n", + " station_id station_name station_lat station_lon \\\n", + "0 s1 Neuperlach Zentrum 48.101125 11.646505 \n", + "1 s2 Jakob-Kaiser-Straße 48.104353 11.640405 \n", + "2 s3 Holzwiesenstraße 48.103075 11.636597 \n", + "3 s4 Wilhelm-Hoegner-Straße 48.098876 11.636399 \n", + "4 s5 Wolframstraße 48.094862 11.635797 \n", + "\n", + " stops_included station_stop_transfer_times \\\n", + "0 ['1-0', '1-1', '1-2', '1-3'] [1, 1, 1, 1] \n", + "1 ['2-0', '2-1'] [1, 1] \n", + "2 ['3-0', '3-1'] [1, 1] \n", + "3 ['4-0', '4-1'] [1, 1] \n", + "4 ['5-0', '5-1'] [1, 1] \n", + "\n", + " num_stops_included \n", + "0 4 \n", + "1 2 \n", + "2 2 \n", + "3 2 \n", + "4 2 \n" + ] + } + ], + "source": [ + "# read example stations file\n", + "stations_fp = pd.read_csv(os.path.join(example_gtfs_path, 'stations_fp.txt'))\n", + "print(\"\\nStations file preview:\")\n", + "print(stations_fp.head())" + ] + }, + { + "cell_type": "markdown", + "id": "4d80550c", + "metadata": {}, + "source": [ + "> #### 🚉 Stations File Requirements (Custom Input)\n", + "> \n", + "> **Role in FleetPy:**\n", + "> Stations serve as the **interface points** between the Road Network and the PT Network.\n", + "> * **Passenger Flow:** Street Network Node $\\rightarrow$ Walk $\\rightarrow$ Station $\\rightarrow$ Transfer $\\rightarrow$ Specific PT Stops (within the station).\n", + "> \n", + "> **Mandatory Columns:**\n", + "> * `station_id`\n", + "> * `stops_included`\n", + "> * `station_stop_transfer_times`\n", + "> * `num_stops_included`\n", + "> \n", + "> **Optional Columns:**\n", + "> * `station_name`\n", + "> * `station_lat`, `station_lon`\n", + "> \n", + "> **Detailed Column Specifications:**\n", + "> * **`station_id`**: A unique `string` used to index the station.\n", + "> * **`stops_included`**: A **list** containing all stop IDs belonging to this station. These IDs must exist in `stops_fp.txt`.\n", + "> * **`station_stop_transfer_times`**: A **list** representing the walking time (in **seconds**) from the abstract Station Node to each corresponding Stop Node.\n", + "> * **`num_stops_included`**: An `integer` representing the total number of stops in the station (i.e., the length of `stops_included`).\n", + "> * **`station_lat` / `station_lon`**: Coordinates of the station. It is recommended that the **CRS** (Coordinate Reference System) matches the network CRS used in your FleetPy experiment.\n", + "> \n", + "> ⚠️ **Implementation Note:**\n", + "> * **Resolution:** Even if your GTFS data (stops_fp.txt) is already at the \"station level\" (rather than stop level), this file **must still be created**. In this case, `station_stop_transfer_times` can be a list containing only zeros.\n", + "> * **Backend Usage:** This file is explicitly called by the `_get_included_stops_and_transfer_times` method in **`RaptorRouterCpp`**." + ] + }, + { + "cell_type": "markdown", + "id": "8bf509e1", + "metadata": {}, + "source": [ + "### street_station_transfers" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "bb8d18d3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Street_station_transfers file preview:\n", + " node_id closest_station_id street_station_transfer_time\n", + "0 2966 s1 60\n", + "1 2967 s2 27\n", + "2 2968 s3 49\n", + "3 2969 s4 27\n", + "4 2970 s5 12\n" + ] + } + ], + "source": [ + "# read example street_station_transfers file\n", + "street_station_transfers_fp = pd.read_csv(os.path.join(example_gtfs_path, 'street_station_transfers_fp.txt'))\n", + "print(\"\\nStreet_station_transfers file preview:\")\n", + "print(street_station_transfers_fp.head())" + ] + }, + { + "cell_type": "markdown", + "id": "9a295e8b", + "metadata": {}, + "source": [ + "> #### 🛣️ Street-Station Transfers File Requirements\n", + "> *Target Module: PTBrokerTPCS*\n", + "> \n", + "> **Mandatory Columns:**\n", + "> * `node_id`\n", + "> * `closest_station_id`\n", + "> * `street_station_transfer_time`\n", + "> \n", + "> **Usage Context:**\n", + "> This file is **mandatory** for the `PTBrokerTPCS` module when the `_find_transfer_info` method is set to the `\"closest\"` query strategy (which is the default setting).\n", + "> \n", + "> **Data Logic:**\n", + "> * **`node_id`**: Represents the PUDO (Pick-up/Drop-off) nodes.\n", + "> * **`closest_station_id`**: Records the ID of the nearest station node relative to the PUDO node.\n", + "> * **`street_station_transfer_time`**: Represents the walking time from the PUDO node to the closest station **in seconds**.\n", + "> \n", + "> ℹ️ **Optimization Note:**\n", + "> If you are exclusively using `RaptorRouterCpp` to run the PT Router (without the PTBrokerTPCS module), this file is unnecessary and can be **ignored**." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "fleetpy", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.19" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/preprocessing/pubtrans/PTScheduleGen.py b/src/preprocessing/pt/PTScheduleGen.py similarity index 98% rename from src/preprocessing/pubtrans/PTScheduleGen.py rename to src/preprocessing/pt/PTScheduleGen.py index f7261423..5d9ea316 100755 --- a/src/preprocessing/pubtrans/PTScheduleGen.py +++ b/src/preprocessing/pt/PTScheduleGen.py @@ -9,7 +9,7 @@ from scipy.stats import poisson from scipy.special import comb import plotly.graph_objects as go -from src.routing.NetworkBasic import NetworkBasic +from src.routing.road.NetworkBasic import NetworkBasic import rtree import math @@ -782,7 +782,7 @@ def plot_route_with_demand(self, terminus_stop: str, time_range=None, data_p = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), "data") demand_csv = os.path.join(data_p, "demand", "SoD_demand", "sample.csv") #'data/demand/SoD_demand/sample.csv' - GTFS_folder = os.path.join(data_p, "pubtrans", "MVG_GTFS_2025-03-04") # "data/pubtrans/MVG_GTFS" + GTFS_folder = os.path.join(data_p, "pt", "MVG_GTFS_2025-03-04") # "data/pt/MVG_GTFS" network_path = os.path.join(data_p, "networks", "osm_route_MVG_road") # "data/networks/osm_route_MVG_road" route_no = 193 @@ -824,7 +824,7 @@ def plot_route_with_demand(self, terminus_stop: str, time_range=None, veh_size = 20 # veh size (passenger) pt_gen.load_demand(demand_csv) - pt_gen.save_alignment_geojson(os.path.join(data_p, "pubtrans", f"route_{route_no}")) + pt_gen.save_alignment_geojson(os.path.join(data_p, "pt", f"route_{route_no}")) hourly_demand = pt_gen.return_hourly_demand(time_range=(start_time, end_time)) print(f"Route {route_no} hourly demand: {hourly_demand}") @@ -835,12 +835,12 @@ def plot_route_with_demand(self, terminus_stop: str, time_range=None, route_len = pt_gen.return_route_length() print(f"Route {route_no} length: {route_len}") - pt_gen.output_station(os.path.join(data_p, "pubtrans", f"route_{route_no}")) + pt_gen.output_station(os.path.join(data_p, "pt", f"route_{route_no}")) # schedule is now standard instead of dependent on headway and n_veh schedule_file_name = f"{route_no}_schedules.csv" veh_type = f"veh_{veh_size}" - pt_gen.output_schedule(os.path.join(data_p, "pubtrans", f"route_{route_no}"), + pt_gen.output_schedule(os.path.join(data_p, "pt", f"route_{route_no}"), schedules_file=schedule_file_name, veh_type=veh_type) gtfs_name = f"route_{route_no}" demand_name = f"route_{route_no}_demand" @@ -848,7 +848,7 @@ def plot_route_with_demand(self, terminus_stop: str, time_range=None, pt_gen.plot_route_with_demand( terminus_stop, time_range=(start_time, end_time), - html_name=os.path.join(data_p, "pubtrans", f"route_{route_no}", f"route_{route_no}_demand_sample.html") + html_name=os.path.join(data_p, "pt", f"route_{route_no}", f"route_{route_no}_demand_sample.html") ) terminus_id = pt_gen.return_terminus_id() diff --git a/src/preprocessing/pubtrans/data/pubtrans/route_193/193_line_alignment.geojson b/src/preprocessing/pt/data/pubtrans/route_193/193_line_alignment.geojson similarity index 100% rename from src/preprocessing/pubtrans/data/pubtrans/route_193/193_line_alignment.geojson rename to src/preprocessing/pt/data/pubtrans/route_193/193_line_alignment.geojson diff --git a/src/preprocessing/pubtrans/data/pubtrans/route_193/stations.csv b/src/preprocessing/pt/data/pubtrans/route_193/stations.csv similarity index 100% rename from src/preprocessing/pubtrans/data/pubtrans/route_193/stations.csv rename to src/preprocessing/pt/data/pubtrans/route_193/stations.csv diff --git a/src/ptctrl/PTControlBase.py b/src/ptctrl/PTControlBase.py new file mode 100644 index 00000000..b1f996ce --- /dev/null +++ b/src/ptctrl/PTControlBase.py @@ -0,0 +1,84 @@ +# -------------------------------------------------------------------------------------------------------------------- # +# standard distribution imports +# ----------------------------- +import logging +import typing as tp +from abc import abstractmethod, ABCMeta + +# additional module imports (> requirements) +# ------------------------------------------ + +# src imports +# ----------- +if tp.TYPE_CHECKING: + from src.routing.pt.RaptorRouterCpp import RaptorRouterCpp + from src.simulation.Offers import PTOffer + +# -------------------------------------------------------------------------------------------------------------------- # +# global variables +# ---------------- +from src.misc.globals import * + +LOG = logging.getLogger(__name__) + +INPUT_PARAMETERS_PTControlBase = { + "doc" : "this class is the base class representing an PT operator", + "inherit" : None, + "input_parameters_mandatory": [], + "input_parameters_optional": [], + "mandatory_modules": [], + "optional_modules": [] +} + +# -------------------------------------------------------------------------------------------------------------------- # +# main +# ---- +class PTControlBase(metaclass=ABCMeta): + @abstractmethod + def __init__(self): + self.pt_router: RaptorRouterCpp = None + self.pt_operator_id: int = None + self.pt_offer_db: tp.Dict[str, 'PTOffer'] = {} + self.gtfs_dir: str = None + + @abstractmethod + def _load_pt_router(self): + """This method will load and initialize the pt router instance. + """ + pass + + @abstractmethod + def return_fastest_pt_journey_1to1(self): + """This method will return the fastest pt journey between an origin and a destination. + """ + pass + + @abstractmethod + def create_and_record_pt_offer_db(self): + """This method will create a TravellerOffer for the pt request and record it in the pt offer database. + """ + pass + + @abstractmethod + def get_current_offer(self): + """This method will return the current offer for the pt request. + """ + pass + + @abstractmethod + def user_confirms_booking(self): + """This method is used to confirm a customer booking. This can trigger some database processes. + """ + pass + + @abstractmethod + def _compute_fare(self): + """This method will compute the fare for the pt request. + """ + pass + + @abstractmethod + def _update_gtfs_data(self): + """This method will update the gtfs data of the pt router to reflect any changes in the pt network or schedule. + """ + pass \ No newline at end of file diff --git a/src/ptctrl/PTControlBasic.py b/src/ptctrl/PTControlBasic.py new file mode 100644 index 00000000..c938a9e5 --- /dev/null +++ b/src/ptctrl/PTControlBasic.py @@ -0,0 +1,197 @@ +# -------------------------------------------------------------------------------------------------------------------- # +# standard distribution imports +# ----------------------------- +import os +import logging +from datetime import datetime +import ast +import typing as tp +import pandas as pd + +# additional module imports (> requirements) +# ------------------------------------------ + +# src imports +# ----------- +from src.ptctrl.PTControlBase import PTControlBase +from src.routing.pt.RaptorRouterCpp import RaptorRouterCpp +from src.simulation.Offers import Rejection, PTOffer +if tp.TYPE_CHECKING: + from src.demand.TravelerModels import BasicIntermodalRequest + +# -------------------------------------------------------------------------------------------------------------------- # +# global variables +# ---------------- +from src.misc.globals import * + +LOG = logging.getLogger(__name__) + +INPUT_PARAMETERS_PTControlBasic = { + "doc" : "this class is the basic PT control class using C++ Raptor implementation", + "inherit" : PTControlBase, + "input_parameters_mandatory": [], + "input_parameters_optional": [], + "mandatory_modules": [RaptorRouterCpp, PTControlBase, Rejection, PTOffer], + "optional_modules": [] +} + +# -------------------------------------------------------------------------------------------------------------------- # +# main +# ---- +class PTControlBasic(PTControlBase): + def __init__(self, gtfs_dir: str, pt_operator_id: int = -2): + super().__init__() + + self.gtfs_dir: str = gtfs_dir + self.pt_operator_id: int = pt_operator_id + self.pt_offer_db: tp.Dict[str, 'PTOffer'] = {} # rid_struct -> PTOffer + self.pt_router: RaptorRouterCpp = self._load_pt_router() + LOG.info("PT operator initialized successfully.") + + def _load_pt_router(self) -> RaptorRouterCpp: + """This method will load and initialize the pt router instance. + """ + return RaptorRouterCpp(self.gtfs_dir) + + def return_fastest_pt_journey_1to1( + self, + source_station_id: str, target_station_id: str, + source_station_departure_datetime: datetime, + max_transfers: int = 999, + detailed: bool = False, + ) -> tp.Union[tp.Dict[str, tp.Any], None]: + """This method will return the fastest pt journey between an origin and a destination. + + Args: + source_station_id (str): id of the source station. + target_station_id (str): id of the target station. + source_station_departure_datetime (datetime): departure datetime from source station. + max_transfers (int, optional): maximum number of transfers allowed. Defaults to 999 (no limit). + detailed (bool, optional): whether to return a detailed journey plan. Defaults to False. + Returns: + tp.Union[tp.Dict[str, tp.Any], None]: The pt journey plan dictionary or None if no journey is found. + """ + pt_journey_plan_dict: tp.Union[tp.Dict[str, tp.Any], None] = self.pt_router.find_fastest_pt_journey_1to1( + source_station_id = source_station_id, + target_station_id = target_station_id, + source_station_departure_datetime = source_station_departure_datetime, + max_transfers = max_transfers, + detailed = detailed, + ) + return pt_journey_plan_dict + + def create_and_record_pt_offer_db( + self, + rid_struct: str, operator_id: int, + source_station_id: str, target_station_id: str, + source_walking_time: int,target_walking_time: int, + pt_journey_plan_dict: tp.Union[tp.Dict[str, tp.Any], None], + firstmile_amod_operator_id: int = None, + ): + """This method will create a PTOffer for the pt request and record it in the pt offer database. + + Args: + rid_struct (str): sub-request id struct of the journey. + operator_id (int): id of PT operator (-2). + source_station_id (str): id of the source station. + target_station_id (str): id of the target station. + source_walking_time (int): walking time [s] from origin street node to source station. + target_walking_time (int): walking time [s] from target station to destination street node. + pt_journey_plan_dict (tp.Union[tp.Dict[str, tp.Any], None]): The pt journey plan dictionary or None if no journey is found. + firstmile_amod_operator_id (int, optional): The operator id of the firstmile amod operator. Defaults to None. + """ + if pt_journey_plan_dict is None: + self.pt_offer_db[(rid_struct, firstmile_amod_operator_id)] = Rejection(rid_struct, operator_id) + else: + fare: int = self._compute_fare() + # old offer will always be overwritten + self.pt_offer_db[(rid_struct, firstmile_amod_operator_id)] = PTOffer( + traveler_id = rid_struct, operator_id = operator_id, + source_station_id = source_station_id, target_station_id = target_station_id, + source_walking_time = source_walking_time, source_station_departure_time = pt_journey_plan_dict.get(G_PT_OFFER_SOURCE_STATION_DEPARTURE_TIME, None), + source_transfer_time = pt_journey_plan_dict.get(G_PT_OFFER_SOURCE_TRANSFER_TIME, None), + waiting_time = pt_journey_plan_dict.get(G_PT_OFFER_SOURCE_WAITING_TIME, None), + trip_time = pt_journey_plan_dict.get(G_PT_OFFER_TRIP_TIME, None), + fare = fare, + target_transfer_time = pt_journey_plan_dict.get(G_PT_OFFER_TARGET_TRANSFER_TIME, None), + target_station_arrival_time = pt_journey_plan_dict.get(G_PT_OFFER_TARGET_STATION_ARRIVAL_TIME, None), + target_walking_time = target_walking_time, + num_transfers = pt_journey_plan_dict.get(G_PT_OFFER_NUM_TRANSFERS, None), + pt_journey_duration = pt_journey_plan_dict.get(G_PT_OFFER_DURATION, None), + detailed_journey_plan = pt_journey_plan_dict.get(G_PT_OFFER_STEPS, None), + ) + + def _compute_fare(self) -> int: + """This method will compute the fare for the pt request. + + For the basic implementation, this method returns 0. + """ + return 0 + + def get_current_offer( + self, + rid_struct: str, + firstmile_amod_operator_id: int = None, + ) -> tp.Optional[PTOffer]: + """This method will return the current offer for the pt request. + + Args: + rid_struct (str): The sub-request id struct of the journey. + firstmile_amod_operator_id (int, optional): The operator id of the firstmile amod operator. Defaults to None. + Returns: + tp.Optional[PTOffer]: The current offer for the pt request. + """ + return self.pt_offer_db.get((rid_struct, firstmile_amod_operator_id), None) + + def user_confirms_booking( + self, + pt_sub_rq_obj: 'BasicIntermodalRequest', + firstmile_amod_operator_id: int = None + ): + """This method is used to confirm a customer booking. This can trigger some database processes. + + Args: + pt_sub_rq_obj (BasicIntermodalRequest): The pt sub-request object. + firstmile_amod_operator_id (int): the id of the firstmile amod operator, only used for FM and FLM requests + """ + pt_rid_struct: str = pt_sub_rq_obj.get_rid_struct() + pt_offer: 'PTOffer' = self.get_current_offer(pt_rid_struct, firstmile_amod_operator_id) + pt_sub_rq_obj.user_boards_vehicle( + simulation_time = pt_offer.get(G_PT_OFFER_SOURCE_STATION_DEPARTURE_TIME, None), + op_id = self.pt_operator_id, + vid = -1, + pu_pos = None, + t_access = pt_offer.get(G_PT_OFFER_SOURCE_WALKING_TIME, None), + ) + pt_sub_rq_obj.user_leaves_vehicle( + simulation_time = pt_offer.get(G_PT_OFFER_TARGET_STATION_ARRIVAL_TIME, None), + do_pos = None, + t_egress = pt_offer.get(G_PT_OFFER_TARGET_WALKING_TIME, None), + ) + + def user_cancels_request( + self, + rid_struct: str, + sim_time: int, + firstmile_amod_operator_id: int = None + ): + """This method is used to cancel a pt request. This removes the offer from the database. + + Args: + rid_struct (str): The sub-request id struct of the journey. + sim_time (int): The simulation time when the cancellation occurs. + firstmile_amod_operator_id (int, optional): The operator id of the firstmile amod operator. Defaults to None. + """ + offer_key = (rid_struct, firstmile_amod_operator_id) + if offer_key in self.pt_offer_db: + del self.pt_offer_db[offer_key] + LOG.debug(f"PT request {rid_struct} cancelled at time {sim_time}") + else: + LOG.debug(f"PT request {rid_struct} not found in offer database, may have been cancelled already") + + def _update_gtfs_data(self): + """This method will update the gtfs data of the pt router to reflect any changes in the pt network or schedule. + + For the basic implementation, this method does nothing. + """ + pass diff --git a/src/routing/pt/RaptorRouterCpp.py b/src/routing/pt/RaptorRouterCpp.py new file mode 100644 index 00000000..a5beee40 --- /dev/null +++ b/src/routing/pt/RaptorRouterCpp.py @@ -0,0 +1,180 @@ +# -------------------------------------------------------------------------------------------------------------------- # +# standard distribution imports +# ----------------------------- +import os +import logging +from datetime import datetime +import ast +import typing as tp +import pandas as pd + +# additional module imports (> requirements) +# ------------------------------------------ + +# src imports +# ----------- +from src.routing.pt.cpp_raptor_router.PyPTRouter import PyPTRouter + +# -------------------------------------------------------------------------------------------------------------------- # +# global variables +# ---------------- +from src.misc.globals import * + +LOG = logging.getLogger(__name__) + +INPUT_PARAMETERS_RaptorRouterCpp = { + "doc" : "this class is the PT router class using C++ Raptor implementation", + "inherit" : [], + "input_parameters_mandatory": [], + "input_parameters_optional": [], + "mandatory_modules": [PyPTRouter], + "optional_modules": [] +} + +# -------------------------------------------------------------------------------------------------------------------- # +# main +# ---- +class RaptorRouterCpp(): + def __init__(self, gtfs_dir: str): + """ + Args: + gtfs_dir (str): the directory path where the GTFS files are stored. + """ + # initialize the pt router + self.pt_router = None + self._initialize_pt_router(gtfs_dir) + + # load the stations: used for station-stop mapping + self.stations_fp_df = self._load_stations_from_gtfs(gtfs_dir) + + def _initialize_pt_router(self, gtfs_dir: str): + """This method initializes the PT router. + + Args: + gtfs_dir (str): the directory path where the GTFS files are stored. + """ + # check if the directories exist and is all mandatory files present + mandatory_files = [ + "agency_fp.txt", + "stops_fp.txt", + "trips_fp.txt", + "routes_fp.txt", + "calendar_fp.txt", + "stop_times_fp.txt", + "stations_fp.txt", + "transfers_fp.txt" + ] + if not os.path.exists(gtfs_dir): + raise FileNotFoundError(f"The directory {gtfs_dir} does not exist.") + for file in mandatory_files: + if not os.path.exists(os.path.join(gtfs_dir, file)): + raise FileNotFoundError(f"The file {file} does not exist in the directory {gtfs_dir}.") + + # initialize the pt router with the given gtfs data + LOG.debug(f"Initializing the Raptor router (C++) with the given GTFS data in the directory: {gtfs_dir}") + self.pt_router = PyPTRouter(gtfs_dir) + LOG.debug("Raptor router (C++) initialized successfully.") + + def _load_stations_from_gtfs(self, gtfs_dir: str) -> pd.DataFrame: + """This method loads the FleetPy-specific stations file. + + Args: + gtfs_dir (str): The directory containing the GTFS data of the operator. + Returns: + pd.DataFrame: The PT stations data. + """ + dtypes = { + 'station_id': 'str', + 'station_name': 'str', + 'station_lat': 'float', + 'station_lon': 'float', + 'stops_included': 'str', + 'station_stop_transfer_times': 'str', + 'num_stops_included': 'int', + } + return pd.read_csv(os.path.join(gtfs_dir, "stations_fp.txt"), dtype=dtypes) + + def find_fastest_pt_journey_1to1( + self, + source_station_id: str, + target_station_id: str, + source_station_departure_datetime: datetime, + max_transfers: int = 999, + detailed: bool = False, + ) -> tp.Union[tp.Dict[str, tp.Any], None]: + """This method returns the fastest PT journey plan between two PT stations. + A station may consist of multiple stops. + + Args: + source_station_id (str): The id of the source station. + target_station_id (str): The id of the target station. + source_station_departure_datetime (datetime): The departure datetime at the source station. + max_transfers (int): The maximum number of transfers allowed in the journey, 999 for no limit. + detailed (bool): Whether to return the detailed journey plan. + Returns: + tp.Union[tp.Dict[str, tp.Any], None]: The fastest PT journey plan or None if no journey is found. + + The returned dictionary contains the following keys: + * 'duration' (int): duration [s] from departure at the source station to arrival at the target station. + * 'trip_time' (int): travel time [s] from departure at the source stop until arrival at the target stop. + * 'num_transfers' (int): number of transfers. + * 'source_station_departure_time' (int): departure timestamp [s] at the source station. + * 'source_station_departure_day' (str): 'current_day' or 'next_day' depending on the departure time. + * 'source_transfer_time' (int): transfer time [s] from the source station to the source stop. + * 'source_waiting_time' (int): waiting time [s] from arrival at the source stop until departure of the trip., + * 'target_transfer_time' (int): transfer time [s] from the target stop to the target station. + * 'target_station_arrival_time' (int):arrival timestamp [s] at the target station. + * 'target_station_arrival_day' (str): 'current_day' or 'next_day' depending on the arrival time. + * 'steps' (List[dict]): a list of journey steps, where each step contains: + - 'duration' (int): step duration [s] from departure at the start stop until arrival at the end stop. + - 'departure_time' (int): departure timestamp [s] at the start stop. + - 'arrival_time' (int): arrival timestamp [s] at the end stop. + - 'from_stop_id' (str): ID of the start stop. + - 'to_stop_id' (str): ID of the end stop. + - ... (other step keys, check the C++ Raptor router documentation for more details). + """ + # get all included stops for the source and target station + included_sources = self._get_included_stops_and_transfer_times(source_station_id) + included_targets = self._get_included_stops_and_transfer_times(target_station_id) + + return self.pt_router.return_fastest_pt_journey_1to1(source_station_departure_datetime, included_sources, included_targets, max_transfers, detailed) + + def _get_included_stops_and_transfer_times(self, station_id: str) -> tp.Tuple[tp.List[str], tp.List[int]]: + """This method returns the included stops and transfer times for a given station. + + Args: + station_id (str): The id of the station. + Returns: + tp.Tuple[tp.List[str], tp.List[int]]: The included stops and transfer times. + """ + station_data = self.stations_fp_df[self.stations_fp_df["station_id"] == station_id] + + if station_data.empty: + raise ValueError(f"Station ID {station_id} not found in the stations data") + + included_ids_str = station_data["stops_included"].iloc[0] + included_ids_str = included_ids_str.replace(';', ',') + included_ids_list = ast.literal_eval(included_ids_str) + + transfer_times_str = station_data["station_stop_transfer_times"].iloc[0] + transfer_times_str = transfer_times_str.replace(';', ',') + transfer_times_list = ast.literal_eval(transfer_times_str) + return [(stop_id, int(transfer_time)) for stop_id, transfer_time in zip(included_ids_list, transfer_times_list)] + + + +if __name__ == "__main__": + # Test/run the pt router module: python -m src.routing.pt.RaptorRouterCpp + # To prepare the GTFS data, please check: src/preprocessing/pt/PTRouterGTFSPreperation.ipynb + + gtfs_dir = "data/pt/example_network/example_gtfs/matched" + router = RaptorRouterCpp(gtfs_dir) + + source_station_departure_datetime = datetime(2024, 1, 1, 0, 4, 0) + import time + start_time = time.time() + print(router.find_fastest_pt_journey_1to1("s1", "s14", source_station_departure_datetime, 3, detailed=False)) + print(f"Time taken: {time.time() - start_time} seconds") + start_time = time.time() + print(router.find_fastest_pt_journey_1to1("s1", "s14", source_station_departure_datetime, 3, detailed=True)) + print(f"Time taken: {time.time() - start_time} seconds") \ No newline at end of file diff --git a/src/routing/pt/cpp_raptor_router/DateTime.h b/src/routing/pt/cpp_raptor_router/DateTime.h new file mode 100644 index 00000000..410a21c8 --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/DateTime.h @@ -0,0 +1,63 @@ +/** + * @file DateTime.h + * @brief Provides data structures for representing dates and times in the RAPTOR application. + * + * This header defines the Date and Time structures, along with auxiliary enums and constants, + * for handling and manipulating temporal data. + * + * @author Maria + * @date 11/20/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ + +#ifndef RAPTOR_DATETIME_H +#define RAPTOR_DATETIME_H + +#include + +/** + * @brief Represents midnight in seconds (24 hours * 3600 seconds per hour). + */ +static constexpr int MIDNIGHT = 24 * 3600; + +/** + * @brief Names of the weekdays starting from Monday. + */ +constexpr const char* weekdays_names[] = {"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}; + +/** + * @struct Date + * @brief Represents a specific date in the Gregorian calendar. + * + * Includes fields for the year, month, day, and the day of the week. + */ +struct Date { + int year; ///< Year of the date (e.g., 2024). + int month; ///< Month of the date (1 = January, ..., 12 = December). + int day; ///< Day of the month (1-31). + int weekday; ///< Day of the week (0 = Monday, 1 = Tuesday, ..., 6 = Sunday). +}; + +/** + * @enum Day + * @brief Represents the current or the next day for calculations. + */ +enum class Day { + CurrentDay, ///< Refers to the current day. + NextDay ///< Refers to the next day. +}; + +/** + * @struct Time + * @brief Represents a specific time of day in hours, minutes, and seconds. + */ +struct Time { + int hours; ///< Hours component of the time (0-23). + int minutes; ///< Minutes component of the time (0-59). + int seconds; ///< Seconds component of the time (0-59). +}; + +#endif //RAPTOR_DATETIME_H diff --git a/src/routing/pt/cpp_raptor_router/NetworkObjects/DataStructures.h b/src/routing/pt/cpp_raptor_router/NetworkObjects/DataStructures.h new file mode 100644 index 00000000..66fc415a --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/NetworkObjects/DataStructures.h @@ -0,0 +1,160 @@ +/** + * @file DataStructures.h + * @brief Defines core data structures and utility classes for the RAPTOR project. + * + * This header file includes declarations for structs like `Query`, `StopInfo`, `JourneyStep`, + * and `Journey`, which are used to represent transit queries, stop information, and journey details. + * It also provides hash functions for specific pair-based keys. + * @author Maria + * @date 11/20/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ + +#ifndef DATASTRUCTURES_H +#define DATASTRUCTURES_H + +#include +#include +#include +#include +#include + +#include "../DateTime.h" + +class Stop; + +/** + * @struct Query + * @brief Represents a transit query. + * + * This structure is used to define a user's query for transit planning, + * including source and target stops, the desired date, and departure time. + */ +struct Query { + std::vector> included_sources; ///< Stops in the source station with its station_stop_transfer_time (in seconds). + std::vector> included_targets; ///< Stops in the target station with its station_stop_transfer_time (in seconds). + Date date; ///< Date of the journey (year, month, day). + Time departure_time; ///< Desired departure time at the source station (in seconds from midnight). + int max_transfers; ///< Maximum number of transfers. +}; + +/** + * @struct StopInfo + * @brief Represents information about a transit stop during a journey. + * + * This structure holds details about a stop's arrival time, the trip and stop + * it depends on, and the day of operation. Values are optional to handle + * cases where a stop is unreachable or is a starting point. + */ +struct StopInfo { + std::optional arrival_seconds; ///< Arrival time in seconds, or `std::nullopt` if unreachable. + std::optional parent_trip_id; ///< ID of the parent trip, or `std::nullopt` for footpaths. + std::optional parent_stop_id; ///< ID of the parent stop, or `std::nullopt` for first stops. + std::optional day; ///< Day of arrival, or `std::nullopt` if unreachable. +}; + +/** + * @struct JourneyStep + * @brief Represents a single step in a journey. + * + * A journey step can correspond to a trip or a footpath. It contains information + * about the source and destination stops, departure and arrival times, and duration. + */ +struct JourneyStep { + std::optional trip_id; ///< ID of the trip, or `std::nullopt` for footpaths. + std::optional agency_name; ///< Name of the agency, or `std::nullopt` for footpaths. + Stop *src_stop{}; ///< Pointer to the source stop. + Stop *dest_stop{}; ///< Pointer to the destination stop. + + int departure_secs{}; ///< Departure time in seconds from midnight of the query day. + Day day{}; ///< Day of the journey step. + int duration{}; ///< Duration of the step in seconds. + int arrival_secs{}; ///< Arrival time in seconds from midnight of the query day. +}; + +/** + * @struct Journey + * @brief Represents an entire journey consisting of multiple steps. + * + * The `Journey` structure contains details about all steps in the journey, + * as well as overall departure and arrival times and durations. + * source_station_departure_secs|source_transfer_time|source_waiting_time|trip_time|target_transfer_time|target_station_arrival_secs + * |------------------------------------------------duration-----------------------------------------------------------------------| + */ +struct Journey { + std::vector steps; ///< Steps making up the journey. + int source_station_departure_secs; ///< Overall departure time in seconds from midnight of the query day at source station. + Day source_station_departure_day; ///< Departure day of the journey at source station. + + int target_station_arrival_secs; ///< Overall arrival time in seconds from midnight of the query day at target station. + Day target_station_arrival_day; ///< Arrival day of the journey at target station. + + int duration; ///< Total duration of the journey from source station to target station in seconds. + int source_transfer_time; ///< Transfer time from source station to source station stop in seconds. + int source_waiting_time; ///< Waiting time at the source station in seconds. + int trip_time; ///< Trip time from source station stop to target station stop in seconds. + int target_transfer_time; ///< Transfer time from target station stop to target station in seconds. + + int num_transfers; ///< Number of transfers in the journey. +}; + +/** + * @struct StopTimeRecord + * @brief Represents a stop time record in the GTFS data. + * + * This struct stores information about a stop time record, including the stop ID, + * arrival time, departure time, and stop sequence. + */ +struct StopTimeRecord { + std::string stop_id; ///< The ID of the stops + int arrival_seconds; ///< The arrival time in seconds + int departure_seconds; ///< The departure time in seconds + int stop_sequence; ///< The sequence number of the stop in the trip + + bool operator<(const StopTimeRecord& other) const { + return stop_sequence < other.stop_sequence; + } +}; + +/** + * @struct pair_hash + * @brief Hash function for a pair of strings. + * + * Provides a custom hash implementation for pairs of strings, + * used in unordered containers like `std::unordered_map` and `std::unordered_set`. + */ +struct pair_hash { + /** + * @brief Computes the hash value for a pair of strings. + * @param pair The pair of strings to hash. + * @return The computed hash value. + */ + std::size_t operator()(const std::pair &pair) const { + return std::hash()(pair.first) ^ std::hash()(pair.second); + } +}; + +/** + * @struct nested_pair_hash + * @brief Hash function for nested pairs of strings. + * + * Provides a custom hash implementation for nested pairs of strings, + * used in unordered containers for hierarchical keys. + */ +struct nested_pair_hash { + /** + * @brief Computes the hash value for a nested pair of strings. + * @param nested_pair The nested pair to hash. + * @return The computed hash value. + */ + std::size_t operator()(const std::pair, std::string> &nested_pair) const { + std::size_t hash1 = pair_hash{}(nested_pair.first); // Hash of internal part + std::size_t hash2 = std::hash{}(nested_pair.second); + return hash1 ^ (hash2 << 1); + } +}; + +#endif //DATASTRUCTURES_H diff --git a/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Agency.cpp b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Agency.cpp new file mode 100644 index 00000000..157b9eb0 --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Agency.cpp @@ -0,0 +1,18 @@ +/** + * @file Agency.cpp + * @brief Implements the Agency class. + * + * This file contains the implementation of the Agency class, which represents + * transit agencies in the GTFS dataset. + * + * @note Currently, this file serves as a placeholder for future extensions. + * + * @author Maria + * @date 11/20/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ + +#include "Agency.h" diff --git a/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Agency.h b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Agency.h new file mode 100644 index 00000000..c486859d --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Agency.h @@ -0,0 +1,35 @@ +/** + * @file Agency.h + * @brief Defines the Agency class, representing transit agencies in the GTFS dataset. + * + * This header file declares the Agency class, which inherits from GTFSObject. + * The class serves as a representation of the GTFS "agency.txt" file, storing + * information about transit agencies. + * + * @author Maria + * @date 11/20/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ +#ifndef RAPTOR_AGENCY_H +#define RAPTOR_AGENCY_H + +#include "GTFSObject.h" + +/** + * @class Agency + * @brief Represents a transit agency in the GTFS data. + * + * This class inherits from GTFSObject and encapsulates the details of a transit agency. + * + * * @note This class currently acts as a placeholder and can be extended + * with specific attributes and methods relevant to transit agencies. + */ +class Agency : public GTFSObject { + +}; + + +#endif //RAPTOR_AGENCY_H diff --git a/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/GTFSObject.cpp b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/GTFSObject.cpp new file mode 100644 index 00000000..f7d6e254 --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/GTFSObject.cpp @@ -0,0 +1,46 @@ +/** + * @file GTFSObject.cpp + * @brief Implements the GTFSObject class. + * + * This file contains the implementation of the GTFSObject class, which represents + * a generic GTFS object. + * + * @author Maria + * @date 11/20/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ + +#include "GTFSObject.h" + +void GTFSObject::setField(const std::string &field, const std::string &value) { + fields[field] = value; +} + +std::string GTFSObject::getField(const std::string &field) const { + auto it = fields.find(field); + if (it == fields.end()) + throw std::runtime_error("Field not found: " + field); + return it->second; +} + +const std::unordered_map >FSObject::getFields() const { + return fields; +} + +bool GTFSObject::hasField(const std::string& field) const { + return fields.find(field) != fields.end(); +} + +void GTFSObject::merge(const GTFSObject &other, bool override) { + for (const auto &[key, value]: other.getFields()) { + if (hasField(key) && !override) { + // Keep the original value + continue; + } else { + fields[key] = value; + } + } +} diff --git a/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/GTFSObject.h b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/GTFSObject.h new file mode 100644 index 00000000..5c7e0738 --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/GTFSObject.h @@ -0,0 +1,76 @@ +/** + * @file GTFSObject.h + * @brief Defines the GTFSObject class, representing a generic GTFS object. + * + * This header file declares the GTFSObject class, + * which serves as a base class for all GTFS objects. + * + * @author Maria + * @date 11/20/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ + +#ifndef RAPTOR_GTFSOBJECT_H +#define RAPTOR_GTFSOBJECT_H + +#include +#include +#include +#include +#include + +#include "../../Utils.h" + +/** + * @class GTFSObject + * @brief Represents a generic GTFS object. + * + * This class serves as a base class for all GTFS objects. + * It provides a generic interface for setting and getting field values. + */ +class GTFSObject { +public: + /** + * @brief Sets the value of a field. + * @param field The name of the field. + * @param value The value to assign to the field. + */ + void setField(const std::string &field, const std::string &value); + + /** + * @brief Retrieves the value of a field. + * @param field The name of the field to retrieve. + * @return The value of the specified field. + * @throws std::runtime_error If the field does not exist. + */ + std::string getField(const std::string &field) const; + + /** + * @brief Gets all fields as an unordered map. + * @return A reference to the map of fields. + */ + const std::unordered_map &getFields() const; + + /** + * @brief Checks if a field exists. + * @param field The name of the field to check. + * @return True if the field exists, false otherwise. + */ + bool hasField(const std::string &field) const; + + /** + * @brief Merges two GTFS objects. + * @param other The GTFS object to merge with. + * @param override Whether to override existing fields. + */ + void merge(const GTFSObject &other, bool override = false); + +protected: + std::unordered_map fields; ///< Map of field names and values. + +}; + +#endif //RAPTOR_GTFSOBJECT_H diff --git a/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Route.cpp b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Route.cpp new file mode 100644 index 00000000..793ad6eb --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Route.cpp @@ -0,0 +1,39 @@ +/** + * @file Route.cpp + * @brief Route class implementation + * + * This file contains the implementation of the Route class, which represents + * a route in the GTFS dataset. + * + * @author Maria + * @date 11/20/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ + +#include "Route.h" +#include + +void Route::addTripId(const std::string& trip_id) { + trips_ids.push_back(trip_id); +} + +void Route::addStopId(const std::string& stop_id) { + stops_ids.insert(stop_id); +} + +void Route::sortTrips(const std::function &comparator) { + std::sort(trips_ids.begin(), trips_ids.end(), comparator); +} + +const std::vector &Route::getTripsIds() const { + return trips_ids; +} + +const std::unordered_set &Route::getStopsIds() const { + return stops_ids; +} + + diff --git a/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Route.h b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Route.h new file mode 100644 index 00000000..8ef0bfda --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Route.h @@ -0,0 +1,68 @@ +/** + * @file Route.h + * @brief Defines the Route class, representing a route in the GTFS dataset. + * + * This header file declares the Route class, which inherits from GTFSObject. + * The class serves as a representation of the GTFS "route.txt" file, storing + * information about a route. + * + * @author Maria + * @date 11/20/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ + +#ifndef RAPTOR_ROUTE_H +#define RAPTOR_ROUTE_H + +#include "GTFSObject.h" + +/** + * @class Route + * @brief Represents a route in the GTFS data. + * + * This class inherits from GTFSObject and manages trip and stop information + * for a specific route. It provides methods for adding trip and stop IDs, + * retrieving sorted data, and defining custom sorting mechanisms. + * + */ +class Route : public GTFSObject { +public: + /** + * @brief Adds a trip ID to the route. + * @param trip_id The ID of the trip to add. + */ + void addTripId(const std::string &trip_id); + + /** + * @brief Adds a stop ID to the route. + * @param stop_id The ID of the stop to add. + */ + void addStopId(const std::string &stop_id); + + /** + * @brief Sorts the trips using a custom comparator. + * @param comparator A function defining the sorting criteria. + */ + void sortTrips(const std::function &comparator); + + /** + * @brief Retrieves the list of trip IDs. + * @return A constant reference to the vector of trip IDs. + */ + const std::vector &getTripsIds() const; + + /** + * @brief Retrieves the list of stop IDs. + * @return A constant reference to the set of stop IDs. + */ + const std::unordered_set &getStopsIds() const; + +private: + std::vector trips_ids; ///< Vector of trip IDs, sorted by earliest arrival time + std::unordered_set stops_ids; ///< Set of stop IDs +}; + +#endif //RAPTOR_ROUTE_H diff --git a/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Service.cpp b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Service.cpp new file mode 100644 index 00000000..6385523a --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Service.cpp @@ -0,0 +1,49 @@ +/** + * @file Service.cpp + * @brief Implements the Service class. + * + * This file contains the implementation of the Service class, which represents + * active days of a service in the GTFS dataset. + * + * @author Maria + * @date 11/20/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ + +#include "Service.h" + +bool Service::isActive(Date date) const { + int date_int = Utils::dateToInt(date); + int start_date = std::stoi(getField("start_date")); + int end_date = std::stoi(getField("end_date")); + + if (included_dates.find(date_int) != included_dates.end()) { + return true; + } + + if (excluded_dates.find(date_int) != excluded_dates.end()) { + return false; + } + + if (date_int < start_date || date_int > end_date) { + return false; + } + + return active_weekdays.find(date.weekday) != active_weekdays.end(); +} + +void Service::addActiveWeekday(int weekday) { + active_weekdays.insert(weekday); +} + +void Service::addExceptionDate(int date_int, int type) { + if (type == 1) { + included_dates.insert(date_int); + } else if (type == 2) { + excluded_dates.insert(date_int); + } +} + diff --git a/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Service.h b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Service.h new file mode 100644 index 00000000..0f5ef262 --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Service.h @@ -0,0 +1,41 @@ +/** + * @file Service.h + * @brief Defines the Service class, representing active days for a service. + * + * This header file declares the Service class, which inherits from GTFSObject. + * The class serves as a representation of the GTFS "calendar.txt" file and "calendar_dates.txt" file, storing + * information about active days of a service. + * + * @author Maria + * @date 11/20/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ + +#ifndef RAPTOR_SERVICE_H +#define RAPTOR_SERVICE_H + +#include "GTFSObject.h" + +/** + * @class Service + * @brief Represents active days for a service in the GTFS data. + * + * This class inherits from GTFSObject and encapsulates the details of active days for a service. + */ +class Service : public GTFSObject { +public: + bool isActive(Date date) const; + void addActiveWeekday(int weekday); + void addExceptionDate(int date_int, int type); + +private: + std::unordered_set active_weekdays; // 0-6, 0 = monday, 6 = sunday + std::unordered_set included_dates; // exception type 1 + std::unordered_set excluded_dates; // exception type 2 +}; + +#endif //RAPTOR_SERVICE_H + diff --git a/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Stop.cpp b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Stop.cpp new file mode 100644 index 00000000..1c399d05 --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Stop.cpp @@ -0,0 +1,43 @@ +/** + * @file Stop.cpp + * @brief Stop class implementation + * + * This file contains the implementation of the Stop class, which represents + * a stop in the GTFS dataset. + * + * @author Maria + * @date 11/20/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ + +#include "Stop.h" + +void Stop::addRouteKey(const std::pair &route_key) { + routes_keys.insert(route_key); +} + +void Stop::addFootpath(const std::string &target_id, int duration) { + footpaths[target_id] = duration; +} + +const std::unordered_set, pair_hash> &Stop::getRouteKeys() const { + return routes_keys; +} + +int Stop::getFootpathTime(const std::string &target_id) const { + if (!hasFootpath(target_id)) + throw std::runtime_error("No footpath to " + target_id); + return footpaths.at(target_id); +} + +bool Stop::hasFootpath(const std::string &target_id) const { + return footpaths.find(target_id) != footpaths.end(); +} + +const std::unordered_map &Stop::getFootpaths() const { + return footpaths; +} + diff --git a/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Stop.h b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Stop.h new file mode 100644 index 00000000..9c47e64b --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Stop.h @@ -0,0 +1,78 @@ +/** + * @file Stop.h + * @brief Defines the Stop class, representing a stop in the GTFS dataset. + * + * This header file declares the Stop class, which inherits from GTFSObject. + * The class serves as a representation of the GTFS "stop.txt" file, storing + * information about a stop. + * + * @author Maria + * @date 11/20/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ + +#ifndef RAPTOR_STOP_H +#define RAPTOR_STOP_H + +#include "GTFSObject.h" +#include "../DataStructures.h" + +/** + * @class Stop + * @brief Represents a stop in the GTFS data. + * + * This class inherits from GTFSObject and manages stop time and route information + * for a specific stop. It provides methods for adding stop time and route IDs, + * retrieving sorted data, and defining custom sorting mechanisms. + * + */ +class Stop : public GTFSObject { +public: + /** + * @brief Adds a route key (route_id, direction_id) to the stop. + * @param route_key A pair representing the route key. + */ + void addRouteKey(const std::pair &route_key); + + /** + * @brief Adds a footpath/transfer to another stop. + * @param target_id The ID of the other stop. + * @param duration The duration of the footpath/transfer in seconds. + */ + void addFootpath(const std::string &target_id, int duration); + + /** + * @brief Retrieves the set of route keys. + * @return A constant reference to the unordered set of route keys. + */ + const std::unordered_set, pair_hash> &getRouteKeys() const; + + /** + * @brief Retrieves the transfer time to another stop. + * @param target_id The ID of the other stop. + * @return The transfer time in seconds. + */ + int getFootpathTime(const std::string &target_id) const; + + /** + * @brief Checks if there is a transfer to another stop. + * @param target_id The ID of the other stop. + * @return True if there is a transfer, false otherwise. + */ + bool hasFootpath(const std::string &target_id) const; + + /** + * @brief Retrieves the map of footpaths. + * @return A constant reference to the map of footpaths. + */ + const std::unordered_map &getFootpaths() const; + +private: + std::unordered_set, pair_hash> routes_keys; ///< Set of route keys (route_id, direction_id) + std::unordered_map footpaths; ///< Map of footpaths to other stops +}; + +#endif //RAPTOR_STOP_H diff --git a/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Trip.cpp b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Trip.cpp new file mode 100644 index 00000000..b1411272 --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Trip.cpp @@ -0,0 +1,64 @@ +/** + * @file Trip.cpp + * @brief Trip class implementation + * + * This file contains the implementation of the Trip class, which represents + * a trip in the GTFS dataset. + * + * @author Maria + * @date 11/20/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ + +#include "Trip.h" + +void Trip::addStopTimeRecord(const StopTimeRecord &record) { + stop_time_records_.push_back(record); +} + +const StopTimeRecord* Trip::getStopTimeRecord(const std::string &stop_id) const { + auto it = std::find_if(stop_time_records_.begin(), stop_time_records_.end(), + [&](const StopTimeRecord &record) { + return record.stop_id == stop_id; + }); + if (it == stop_time_records_.end()) { + return nullptr; + } + return &(*it); +} + +const std::vector &Trip::getStopTimeRecords() const { + return stop_time_records_; +} + +void Trip::sortStopTimeRecords() { + std::sort(stop_time_records_.begin(), stop_time_records_.end()); + is_sorted_ = true; +} + +std::vector Trip::getStopTimeRecordsAfter(const std::string &stop_id) const { + auto it = std::find_if(stop_time_records_.begin(), stop_time_records_.end(), [&](const StopTimeRecord &record) { + return record.stop_id == stop_id; + }); + + if (it == stop_time_records_.end()) { + return {}; + } + + return std::vector(it + 1, stop_time_records_.end()); +} + +bool Trip::isActive(Day day) const { + return active_days_.at(day); +} + +void Trip::setActive(Day day, bool is_active) { + active_days_[day] = is_active; +} + +bool Trip::isSorted() const { + return is_sorted_; +} diff --git a/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Trip.h b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Trip.h new file mode 100644 index 00000000..0c72d55e --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/NetworkObjects/GTFSObjects/Trip.h @@ -0,0 +1,91 @@ +/** + * @file Trip.h + * @brief Defines the Trip class, representing a trip in the GTFS dataset. + * + * This header file declares the Trip class, which inherits from GTFSObject. + * The class serves as a representation of the GTFS "trip.txt" file, storing + * information about a trip. + * + * @author Maria + * @date 11/20/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ + +#ifndef RAPTOR_TRIP_H +#define RAPTOR_TRIP_H + +#include "GTFSObject.h" +#include "../DataStructures.h" + +/** + * @class Trip + * @brief Represents a trip in the GTFS data. + * + * This class inherits from GTFSObject and manages stop time information + * for a specific trip. It provides methods for adding stop time records, + * retrieving sorted data, and defining custom sorting mechanisms. + * + */ +class Trip : public GTFSObject { +public: + /** + * @brief Adds a stop time record to the trip. + * @param record The stop time record to add. + */ + void addStopTimeRecord(const StopTimeRecord &record); + + /** + * @brief Retrieves the stop time record for a specific stop. + * @param stop_id The ID of the stop to retrieve the stop time record for. + * @return A constant pointer to the stop time record for the specified stop. If the stop is not found, returns nullptr. + */ + const StopTimeRecord* getStopTimeRecord(const std::string &stop_id) const; + + /** + * @brief Retrieves the stop time records. + * @return A constant reference to the stop time records. + */ + const std::vector &getStopTimeRecords() const; + + /** + * @brief Sorts the stop time records. + */ + void sortStopTimeRecords(); + + /** + * @brief Retrieves the stop time records after a specific stop. + * @param stop_id The ID of the stop to retrieve the stop time records after. + * @return A vector of stop time records after the specified stop. + */ + std::vector getStopTimeRecordsAfter(const std::string &stop_id) const; + + /** + * @brief Sets the active status for a specific day. + * @param day + * @param is_active + */ + void setActive(Day day, bool is_active); + + /** + * @brief Checks if a specific day is active. + * @param day + * @return True if the day is active, false otherwise. + */ + bool isActive(Day day) const; + + /** + * @brief Checks if the stop time records are sorted. + * @return True if the stop time records are sorted, false otherwise. + */ + bool isSorted() const; + +private: + std::unordered_map active_days_; ///< Map of active days for the trip + std::vector stop_time_records_; ///< Vector of stop-time records, sorted by stopTime's sequence + bool is_sorted_; ///< Whether the stop time records are sorted +}; + +#endif //RAPTOR_TRIP_H diff --git a/src/routing/pt/cpp_raptor_router/Parser.cpp b/src/routing/pt/cpp_raptor_router/Parser.cpp new file mode 100644 index 00000000..0b1d1704 --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/Parser.cpp @@ -0,0 +1,444 @@ +/** + * @file Parser.cpp + * @brief Implementation of the Parser class + * + * This file contains the implementation of the Parser class, + * which is responsible for parsing GTFS data. + * + * @author Maria + * @date 11/20/2024 + * + * @author Maria + * @date 11/20/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ + +#include "Parser.h" + +Parser::Parser(std::string directory) : inputDirectory(std::move(directory)) { + // // Record the start time + // auto start_time = std::chrono::high_resolution_clock::now(); + + // std::cout << "Parsing GTFS data from " << inputDirectory << "..." << std::endl; + parseData(); + + // std::cout << "Associating data..." << std::endl; + associateData(); + + // // Record the end time + // auto end_time = std::chrono::high_resolution_clock::now(); + + // // Calculate the duration + // auto duration = std::chrono::duration_cast(end_time - start_time); + + // std::cout << std::fixed << std::setprecision(2) + // << "Parsing completed in " << (duration.count() / 1000.0) << " seconds." << std::endl; +} + +void Parser::parseData() { + //The order of parsing is important due + // std::cout << "Parsing agencies..." << std::endl; + parseAgencies(); + + // std::cout << "Parsing calendars and calendar dates..." << std::endl; + parseServices(); + + // std::cout << "Parsing stop times..." << std::endl; + parseStopTimes(); + + // std::cout << "Parsing trips..." << std::endl; + parseTrips(); + + // std::cout << "Parsing routes..." << std::endl; + parseRoutes(); + + // std::cout << "Parsing stops..." << std::endl; + parseStops(); + + // std::cout << "Parsing transfers..." << std::endl; + parseTransfers(); +} + +void Parser::parseAgencies() { + std::ifstream file(inputDirectory + "/agency_fp.txt"); + + if (!file.is_open()) + throw std::runtime_error("Could not open agency_fp.txt"); + + std::string line; + std::getline(file, line); + Utils::clean(line); + std::vector fields = Utils::split(line, ','); + + while (std::getline(file, line)) { + Utils::clean(line); + + if (line.empty()) continue; + + std::vector tokens = Utils::split(line, ','); + + if (tokens.size() != fields.size()) + throw std::runtime_error("Mismatched number of tokens and fields"); + + Agency agency; + + for (size_t i = 0; i < fields.size(); ++i) + agency.setField(fields[i], tokens[i]); + + agencies_[agency.getField("agency_id")] = agency; + } +} + +void Parser::parseServices() { + // 1. Parse calendar_fp.txt + std::ifstream file(inputDirectory + "/calendar_fp.txt"); + if (!file.is_open()) + throw std::runtime_error("Could not open calendar_fp.txt"); + + std::string line; + std::getline(file, line); + Utils::clean(line); + std::vector fields = Utils::split(line, ','); + + while (std::getline(file, line)) { + Utils::clean(line); + + if (line.empty()) continue; + + std::vector tokens = Utils::split(line, ','); + + if (tokens.size() != fields.size()) + throw std::runtime_error("Mismatched number of tokens and fields"); + + Service service; + + for (size_t i = 0; i < fields.size(); ++i) + service.setField(fields[i], tokens[i]); + + // Register active_weekdays: 0-6, 0 = monday, 6 = sunday + for (int i = 0; i < 7; ++i) { + std::string weekdays_name = weekdays_names[i]; + if (service.getField(weekdays_name) == "1") { + service.addActiveWeekday(i); + } + } + + services_[service.getField("service_id")] = service; + } + + // 2. Parse calendar_dates.txt + std::ifstream dates_file(inputDirectory + "/calendar_dates_fp.txt"); + if (!dates_file.is_open()) + // This file is an optional file, so we don't throw an error if it's not found + return; + + std::getline(dates_file, line); + Utils::clean(line); + fields = Utils::split(line, ','); + + while (std::getline(dates_file, line)) { + Utils::clean(line); + + if (line.empty()) continue; + + std::vector tokens = Utils::split(line, ','); + + if (tokens.size() != fields.size()) + throw std::runtime_error("Mismatched number of tokens and fields"); + + GTFSObject calendar_date; + + for (size_t i = 0; i < fields.size(); ++i) + calendar_date.setField(fields[i], tokens[i]); + + std::string service_id = calendar_date.getField("service_id"); + + if (services_.find(service_id) != services_.end()) { + int date = std::stoi(calendar_date.getField("date")); + int type = std::stoi(calendar_date.getField("exception_type")); + + services_[service_id].addExceptionDate(date, type); + } + } +} + +void Parser::parseStopTimes() { + std::ifstream file(inputDirectory + "/stop_times_fp.txt"); + if (!file.is_open()) + throw std::runtime_error("Could not open stop_times_fp.txt"); + + std::string line; + std::getline(file, line); + Utils::clean(line); + std::vector fields = Utils::split(line, ','); + + while (std::getline(file, line)) { + Utils::clean(line); + + if (line.empty()) continue; + + std::vector tokens = Utils::split(line, ','); + + if (tokens.size() != fields.size()) + throw std::runtime_error("Mismatched number of tokens and fields"); + + GTFSObject stop_time; + + for (size_t i = 0; i < fields.size(); ++i) + stop_time.setField(fields[i], tokens[i]); + + std::string trip_id = stop_time.getField("trip_id"); + std::string stop_id = stop_time.getField("stop_id"); + int stop_sequence = std::stoi(stop_time.getField("stop_sequence")); + // Convert arrival_time and departure_time to seconds + // If the departure_time is on the next day, add 24 hours to it + int arrival_seconds = Utils::timeToSeconds(stop_time.getField("arrival_time")); + int departure_seconds = Utils::timeToSeconds(stop_time.getField("departure_time")); + if (departure_seconds < arrival_seconds) { + departure_seconds += MIDNIGHT; + } + + // 1. Register active trip ids and corresponding stop time records + StopTimeRecord record = {stop_id, arrival_seconds, departure_seconds, stop_sequence}; + + if (trips_.find(trip_id) == trips_.end()) { + Trip trip; + trips_[trip_id] = trip; + } + trips_[trip_id].addStopTimeRecord(record); + + // 2. Register active stop ids + if (stops_.find(stop_id) == stops_.end()) { + Stop stop; + stops_[stop_id] = stop; + } + } +} + +void Parser::parseTrips() { + std::ifstream file(inputDirectory + "/trips_fp.txt"); + if (!file.is_open()) + throw std::runtime_error("Could not open trips_fp.txt"); + + std::string line; + std::getline(file, line); + Utils::clean(line); + std::vector fields = Utils::split(line, ','); + + while (std::getline(file, line)) { + Utils::clean(line); + + if (line.empty()) continue; + + std::vector tokens = Utils::split(line, ','); + + if (tokens.size() != fields.size()) + throw std::runtime_error("Mismatched number of tokens and fields"); + + Trip trip; + + for (size_t i = 0; i < fields.size(); ++i) + trip.setField(fields[i], tokens[i]); + + // Only parse trips that are effective + std::string trip_id = trip.getField("trip_id"); + if (trips_.find(trip_id) != trips_.end()) { + std::string route_id = trip.getField("route_id"); + std::string direction_id = trip.getField("direction_id"); + + trips_[trip_id].setField("route_id", route_id); + trips_[trip_id].setField("direction_id", direction_id); + trips_[trip_id].setField("service_id", trip.getField("service_id")); + + // Register effective routes + auto route_key = std::make_pair(route_id, direction_id); + if (routes_.find(route_key) == routes_.end()) { + Route route; + routes_[route_key] = route; + } + routes_[route_key].addTripId(trip_id); + } + } +} + +void Parser::parseRoutes() { + std::ifstream file(inputDirectory + "/routes_fp.txt"); + + if (!file.is_open()) + throw std::runtime_error("Could not open routes_fp.txt"); + + std::string line; + std::getline(file, line); + Utils::clean(line); + std::vector fields = Utils::split(line, ','); + + while (std::getline(file, line)) { + Utils::clean(line); + + if (line.empty()) continue; + + std::vector tokens = Utils::split(line, ','); + + if (tokens.size() != fields.size()) + throw std::runtime_error("Mismatched number of tokens and fields"); + + Route route; + for (size_t i = 0; i < fields.size(); ++i) + route.setField(fields[i], tokens[i]); + + // If there is only one agency, agency_id field is optional + if (!route.hasField("agency_id")) + route.setField("agency_id", agencies_.begin()->second.getField("agency_id")); + + // Iterate through all existing (route_id, direction_id) pairs in routes_ + for (auto &[key, r]: routes_) { + auto [route_id, direction_id] = key; + if (route_id == route.getField("route_id")) { + // Merge two routes with the same route_id, keep the original route + r.merge(route, false); + } + } + } +} + +void Parser::parseStops() { + std::ifstream file(inputDirectory + "/stops_fp.txt"); + + if (!file.is_open()) + throw std::runtime_error("Could not open stops_fp.txt"); + + std::string line; + std::getline(file, line); + Utils::clean(line); + std::vector fields = Utils::split(line, ','); + + while (std::getline(file, line)) { + Utils::clean(line); + + if (line.empty()) continue; + + std::vector tokens = Utils::split(line, ','); + + if (tokens.size() != fields.size()) + throw std::runtime_error("Mismatched number of tokens and fields"); + + Stop stop; + for (size_t i = 0; i < fields.size(); ++i) + stop.setField(fields[i], tokens[i]); + + std::string stop_id = stop.getField("stop_id"); + + if (stops_.find(stop_id) != stops_.end()) { + stops_[stop_id].merge(stop, false); + } + } +} + +void Parser::parseTransfers() { + std::ifstream file(inputDirectory + "/transfers_fp.txt"); + if (!file.is_open()) + throw std::runtime_error("Could not open transfers_fp.txt"); + + std::string line; + std::getline(file, line); + Utils::clean(line); + std::vector fields = Utils::split(line, ','); + + while (std::getline(file, line)) { + Utils::clean(line); + + if (line.empty()) continue; + + std::vector tokens = Utils::split(line, ','); + + if (tokens.size() != fields.size()) + throw std::runtime_error("Mismatched number of tokens and fields"); + + GTFSObject transfer; + for (size_t i = 0; i < fields.size(); ++i) + transfer.setField(fields[i], tokens[i]); + + int min_transfer_time = static_cast(std::stof(transfer.getField("min_transfer_time"))); + + std::string from_stop_id = transfer.getField("from_stop_id"); + std::string to_stop_id = transfer.getField("to_stop_id"); + + if (stops_.find(from_stop_id) != stops_.end() && + stops_.find(to_stop_id) != stops_.end()) { + stops_[from_stop_id].addFootpath(to_stop_id, min_transfer_time); + } + } +} + +void Parser::associateData() { + // 0. Check data effectiveness of all data structures + + + // 1. Sort trip time records in stop_times_ + for (auto &[key, trip]: trips_) { + trip.sortStopTimeRecords(); + } + + // 2. Update routes_ with trips_ + for (auto &[route_key, route]: routes_) { + // Sort trips in the route by arrival time of the first stop time record + route.sortTrips( + [&](const std::string &a, const std::string &b) { + const Trip &tripA = trips_.at(a); + const Trip &tripB = trips_.at(b); + + // Find the first stop_time record of the trip + const StopTimeRecord &stopTimeA = tripA.getStopTimeRecords().front(); + const StopTimeRecord &stopTimeB = tripB.getStopTimeRecords().front(); + + int timeA = stopTimeA.arrival_seconds; + int timeB = stopTimeB.arrival_seconds; + + return timeA < timeB; + } + ); + + // Find all possible stops ids in the route + for (const auto &trip_id: route.getTripsIds()) { + const Trip &trip = trips_[trip_id]; + for (const auto &stop_time_record: trip.getStopTimeRecords()) { + route.addStopId(stop_time_record.stop_id); + } + } + } + + // routes_[{"R01", "0"}] = { + // trips_ids: ["T101", "T102", "T103"], + // stops_ids: ["S001", "S002", "S004", "S005"] + // }; + + // 3. Associate routes to stops + for (auto &[route_key, route]: routes_) { + for (const auto &stop_id: route.getStopsIds()) { + stops_[stop_id].addRouteKey(route_key); + } + } +} + +std::unordered_map Parser::getAgencies() { + return agencies_; +} + +std::unordered_map Parser::getServices() { + return services_; +} + +std::unordered_map Parser::getStops() { + return stops_; +} + +std::unordered_map, Route, pair_hash> Parser::getRoutes() { + return routes_; +} + +std::unordered_map Parser::getTrips() { + return trips_; +} \ No newline at end of file diff --git a/src/routing/pt/cpp_raptor_router/Parser.h b/src/routing/pt/cpp_raptor_router/Parser.h new file mode 100644 index 00000000..6715fbf2 --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/Parser.h @@ -0,0 +1,154 @@ +/** + * @file Parser.h + * @brief Provides the Parser class for parsing GTFS data files. + * + * This header file declares the Parser class, which is responsible for parsing + * GTFS data files and associating the data to construct a transit network. + * + * @author Maria + * @date 11/20/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ + +#ifndef PARSE_H +#define PARSE_H + +#include // for file input +#include // for string stream +#include // for input and output +#include // for timing + +#include "Utils.h" // for hash functions +#include "DateTime.h" // for Date and Time + +#include "NetworkObjects/GTFSObjects/GTFSObject.h" // for GTFSObject +#include "NetworkObjects/DataStructures.h" // for DataStructures +#include "NetworkObjects/GTFSObjects/Agency.h" // for Agency +#include "NetworkObjects/GTFSObjects/Service.h" // for Calendar and CalendarDate +#include "NetworkObjects/GTFSObjects/Route.h" // for Route +#include "NetworkObjects/GTFSObjects/Stop.h" // for Stop +#include "NetworkObjects/GTFSObjects/Trip.h" // for Trip + +/** + * @class Parser + * @brief Class for parsing GTFS data files and organizing the information. + * + * This class is responsible for parsing various GTFS data files such as agencies, calendars, stops, routes, + * trips, and stop times. It stores the parsed data in appropriate data structures and allows access to the + * parsed information. + */ +class Parser { +private: + + std::string inputDirectory; /**< Directory where the input files are located. */ + + /** + * Maps to store parsed data. + */ + std::unordered_map agencies_; ///< A map from agency IDs to Agency objects. + std::unordered_map services_; ///< A map from service IDs to Service objects. + std::unordered_map stops_; ///< A map from stop IDs to Stop objects. + std::unordered_map, Route, pair_hash> routes_; ///< A map from (route_id, direction_id) to Route objects. + std::unordered_map trips_; ///< A map from trip IDs to Trip objects. + + /** + *@brief Parses the GTFS data files and stores the results in the appropriate maps. + */ + void parseData(); + + /** + * @brief Parses the agencies file and stores the results in the agencies_ map. + */ + void parseAgencies(); + + /** + * @brief Parses the calendars file and the calendar_dates file, + * stores the results in the services_ map. + */ + void parseServices(); + + /** + * @brief Parses the stop times file and stores the results in the stop_times_ map. + */ + void parseStopTimes(); + + /** + * @brief Parses the trips file and stores the results in the trips_ map. + */ + void parseTrips(); + + /** + * @brief Parses the routes file and stores the results in the routes_ map. + */ + void parseRoutes(); + + /** + * @brief Parses the stops file and stores the results in the stops_ map. + */ + void parseStops(); + + /** + * @brief Parses the transfers file and stores the results in the transfers_ map. + */ + void parseTransfers(); + + /** + * @brief Associates data across various GTFS components (routes, trips, stops, etc.). + * + * This method processes the data from different GTFS files and associates the relevant information + * such as matching trips with corresponding stops and stop times. + */ + void associateData(); + +public: + + /** + * @brief Constructor for the Parser class. + * + * Initializes the parser with the specified directory containing the GTFS data files. + * + * @param[in] directory Path to the directory containing the GTFS files. + */ + explicit Parser(std::string directory); + + /** + * @brief Gets the parsed agencies data. + * + * @return A map of agency IDs to Agency objects. + */ + [[nodiscard]] std::unordered_map getAgencies(); + + /** + * @brief Gets the parsed services data. + * + * @return A map of service IDs to Service objects. + */ + [[nodiscard]] std::unordered_map getServices(); + + + /** + * @brief Gets the parsed stops data. + * + * @return A map of stop IDs to Stop objects. + */ + [[nodiscard]] std::unordered_map getStops(); + + /** + * @brief Gets the parsed routes data. + * + * @return A map of (route_id, direction_id) pairs to Route objects. + */ + [[nodiscard]] std::unordered_map, Route, pair_hash> getRoutes(); + + /** + * @brief Gets the parsed trips data. + * + * @return A map of trip IDs to Trip objects. + */ + [[nodiscard]] std::unordered_map getTrips(); +}; + +#endif //PARSE_H diff --git a/src/routing/pt/cpp_raptor_router/PyPTRouter.pxd b/src/routing/pt/cpp_raptor_router/PyPTRouter.pxd new file mode 100644 index 00000000..f9847660 --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/PyPTRouter.pxd @@ -0,0 +1,122 @@ +# cython: language_level=3 +from libcpp.string cimport string +from libcpp.vector cimport vector +from libcpp.unordered_map cimport unordered_map +from libcpp.utility cimport pair +from libcpp.optional cimport optional +from libcpp cimport bool + +cdef extern from "DateTime.h": + cdef enum class Day: + CurrentDay + NextDay + + cdef struct Date: + int year + int month + int day + int weekday + + cdef struct Time: + int hours + int minutes + int seconds + +cdef extern from "NetworkObjects/DataStructures.h": + cdef struct pair_hash: + pass + + cdef struct nested_pair_hash: + pass + + cdef struct Query: + vector[pair[string, int]] included_sources + vector[pair[string, int]] included_targets + Date date + Time departure_time + int max_transfers + + cdef struct StopInfo: + optional[int] arrival_seconds + optional[string] parent_trip_id + optional[string] parent_stop_id + optional[Day] day + + cdef struct JourneyStep: + optional[string] trip_id + optional[string] agency_name + Stop* src_stop + Stop* dest_stop + int departure_secs + Day day + int duration + int arrival_secs + + cdef struct Journey: + vector[JourneyStep] steps + int source_station_departure_secs + Day source_station_departure_day + int target_station_arrival_secs + Day target_station_arrival_day + int duration + int source_transfer_time + int source_waiting_time + int trip_time + int target_transfer_time + int num_transfers + +cdef extern from "NetworkObjects/GTFSObjects/GTFSObject.h": + cdef cppclass GTFSObject: + void setField(const string& field, const string& value) + string getField(const string& field) const + const unordered_map[string, string]& getFields() const + bool hasField(const string& field) const + +cdef extern from "NetworkObjects/GTFSObjects/Stop.h": + cdef cppclass Stop(GTFSObject): + void addRouteKey(const pair[string, string]& route_key) + void addFootpath(const string& target_id, int duration) + const unordered_map[string, int]& getFootpaths() const + int getFootpathTime(const string& target_id) const + bool hasFootpath(const string& target_id) const + +cdef extern from "NetworkObjects/GTFSObjects/Agency.h": + cdef cppclass Agency(GTFSObject): + pass + +cdef extern from "NetworkObjects/GTFSObjects/Service.h": + cdef cppclass Service(GTFSObject): + pass + +cdef extern from "NetworkObjects/GTFSObjects/Route.h": + cdef cppclass Route(GTFSObject): + pass + +cdef extern from "NetworkObjects/GTFSObjects/Trip.h": + cdef cppclass Trip(GTFSObject): + pass + +cdef extern from "Parser.h": + cdef cppclass Parser: + Parser(string directory) + unordered_map[string, Agency] getAgencies() + unordered_map[string, Service] getServices() + unordered_map[string, Stop] getStops() + unordered_map[pair[string, string], Route, pair_hash] getRoutes() + unordered_map[string, Trip] getTrips() + +cdef extern from "Raptor.h": + cdef cppclass Raptor: + Raptor() + Raptor(const unordered_map[string, Agency]& agencies_, + const unordered_map[string, Service]& services_, + const unordered_map[string, Stop]& stops, + const unordered_map[pair[string, string], Route, pair_hash]& routes, + const unordered_map[string, Trip]& trips) + void setQuery(const Query& query) + vector[Journey] findJourneys() + optional[Journey] findOptimalJourney() + @staticmethod + void showJourney(const Journey& journey) + const unordered_map[string, Stop]& getStops() const + bool isValidJourney(Journey journey) const \ No newline at end of file diff --git a/src/routing/pt/cpp_raptor_router/PyPTRouter.pyx b/src/routing/pt/cpp_raptor_router/PyPTRouter.pyx new file mode 100644 index 00000000..daf46cc2 --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/PyPTRouter.pyx @@ -0,0 +1,291 @@ +# cython: language_level=3 +from libcpp.string cimport string +from libcpp.vector cimport vector +from libcpp.unordered_map cimport unordered_map +from libcpp.utility cimport pair +from libcpp.optional cimport optional +import json +from datetime import datetime + +cdef class PyPTRouter: + cdef Raptor* raptor_ptr # Hold a pointer to the C++ instance which we're wrapping + + def __cinit__(self, str input_directory): + """Initialize the RAPTOR router + + This is a Cython-specific constructor that is called first during object creation. + It initializes the C++ RAPTOR router by loading and processing input data. + + Args: + input_directory (str): Directory path containing GTFS data. + The directory should contain necessary GTFS files + (e.g., stops.txt, routes.txt, etc.) + + Notes: + - This method is automatically called during object creation + - If raptor_ptr already exists, it will be deleted before creating a new instance + - The input path will be converted to UTF-8 encoded C++ string + """ + # Convert Python string to C++ string + cdef string cpp_directory = input_directory.encode('utf-8') + + # Initialize data containers for GTFS data + cdef unordered_map[string, Agency] agencies + cdef unordered_map[string, Service] services + cdef unordered_map[string, Trip] trips + cdef unordered_map[pair[string, string], Route, pair_hash] routes + cdef unordered_map[string, Stop] stops + + # Process directory + cdef Parser* parser = new Parser(cpp_directory) + + agencies = parser.getAgencies() + services = parser.getServices() + trips = parser.getTrips() + routes = parser.getRoutes() + stops = parser.getStops() + + del parser + + # Clean up existing instance if any + if self.raptor_ptr != NULL: + del self.raptor_ptr + + # Create new RAPTOR instance with data + self.raptor_ptr = new Raptor(agencies, services, stops, routes, trips) + + def __dealloc__(self): + """Deallocate the RAPTOR router + + This is a Cython-specific destructor that is called when the object is garbage collected. + It ensures proper cleanup of C++ resources to prevent memory leaks. + + Notes: + - Automatically called during garbage collection + - Safely deletes the C++ Raptor object if it exists + - Sets raptor_ptr to NULL after deletion is handled by C++ + """ + if self.raptor_ptr != NULL: + del self.raptor_ptr + + def construct_query( + self, + source_station_departure_datetime, + list included_sources, list included_targets, int max_transfers=-1, + ): + """Construct query information. + + Args: + source_station_departure_datetime (datetime): Departure datetime at the source station + included_sources (list): List of source stop IDs and their station stop transfer times + included_targets (list): List of target stop IDs and their station stop transfer times + max_transfers (int): Maximum number of transfers allowed + + Returns: + query (Query) + """ + # Calculate day of week using Python's datetime + cdef int year = source_station_departure_datetime.year + cdef int month = source_station_departure_datetime.month + cdef int day = source_station_departure_datetime.day + cdef int weekday = source_station_departure_datetime.weekday() + cdef int hours = source_station_departure_datetime.hour + cdef int minutes = source_station_departure_datetime.minute + cdef int seconds = source_station_departure_datetime.second + + # Create date, time objects for C++ + cdef Date date = Date(year, month, day, weekday) + cdef Time departure_time = Time(hours, minutes, seconds) + + cdef Query query + + cdef vector[pair[string, int]] src_vec + cdef vector[pair[string, int]] tgt_vec + + src_vec = vector[pair[string, int]]() + for inc_src in included_sources: + if isinstance(inc_src, tuple) and len(inc_src) == 2 and isinstance(inc_src[0], str) and isinstance(inc_src[1], int): + src_vec.push_back(pair[string, int](inc_src[0].encode('utf-8'), inc_src[1])) + else: + raise TypeError(f"Expected (string, int) tuple, got {type(inc_src)}") + + tgt_vec = vector[pair[string, int]]() + for inc_tgt in included_targets: + if isinstance(inc_tgt, tuple) and len(inc_tgt) == 2 and isinstance(inc_tgt[0], str) and isinstance(inc_tgt[1], int): + tgt_vec.push_back(pair[string, int](inc_tgt[0].encode('utf-8'), inc_tgt[1])) + else: + raise TypeError(f"Expected (string, int) tuple, got {type(inc_tgt)}") + + return Query(src_vec, tgt_vec, date, departure_time, max_transfers) + + def return_pt_journeys_1to1( + self, + source_station_departure_datetime, + list included_sources, list included_targets, int max_transfers=-1, + bool detailed=False, + ): + """Find the best public transport journey from source to target + + This method queries the RAPTOR router to find the optimal journey between two stops + at a specified departure time. + + Args: + source_station_departure_datetime (datetime): Departure datetime at the source station + included_sources (list): List of source stop IDs and their station stop transfer times + included_targets (list): List of target stop IDs and their station stop transfer times + max_transfers (int): Maximum number of transfers allowed (-1 for unlimited) + detailed (bool): Whether to return the detailed journey plan. + + Returns: + dict: A dictionary containing journey details, or None if no journey is found. + The dictionary includes: + - duration: Total journey duration in seconds + """ + if self.raptor_ptr == NULL: + raise RuntimeError("RAPTOR router not initialized. Please initialize first.") + + query = self.construct_query( + source_station_departure_datetime, included_sources, included_targets, max_transfers, + ) + + # Set query and find journeys + self.raptor_ptr.setQuery(query) + cdef vector[Journey] journeys = self.raptor_ptr.findJourneys() + + # Check if any journeys were found + if journeys.size() == 0: + return None + + # Convert all journeys to Python list of dictionaries + journeys_list = [] + for i in range(journeys.size()): + journey_dict = self._convert_journey_to_dict(journeys[i], detailed) + journeys_list.append(journey_dict) + + return journeys_list + + def return_fastest_pt_journey_1to1( + self, + source_station_departure_datetime, + list included_sources, list included_targets, int max_transfers=-1, + bool detailed=False, + ): + """Find the fastest public transport journey from source station to target station + + Args: + source_station_departure_datetime (datetime): Departure datetime at the source station + included_sources (list): List of source stop IDs and their station stop transfer times + included_targets (list): List of target stop IDs and their station stop transfer times + max_transfers (int): Maximum number of transfers allowed (-1 for unlimited) + detailed (bool): Whether to return the detailed journey plan. + + Returns: + dict: A dictionary containing journey details, or None if no journey is found. + The dictionary includes: + - duration: Total journey duration in seconds + """ + if self.raptor_ptr == NULL: + raise RuntimeError("RAPTOR router not initialized. Please initialize first.") + + query = self.construct_query( + source_station_departure_datetime, included_sources, included_targets, max_transfers, + ) + + # Set query and find journeys + self.raptor_ptr.setQuery(query) + cdef optional[Journey] journey_opt = self.raptor_ptr.findOptimalJourney() + + # Check if journey was found + if not journey_opt.has_value(): + return None + + # Get the actual journey from optional + cdef Journey journey = journey_opt.value() + + # Convert journey to Python dictionary + journey_dict = self._convert_journey_to_dict(journey, detailed) + + return journey_dict + + cdef _convert_journey_to_dict(self, Journey journey, bool detailed): + """Convert a Journey object to a Python dictionary + + This method takes a C++ Journey object and converts it to a Python dictionary + with all the relevant journey information. + + Args: + journey (Journey): The C++ Journey object to convert + detailed (bool): Whether to include the detailed journey plan + + Returns: + dict: A dictionary containing journey details + """ + journey_dict = { + # Overall journey information + "duration": journey.duration, + "trip_time": journey.trip_time, + "num_transfers": journey.num_transfers, + + # Departure information + "source_transfer_time": journey.source_transfer_time, + "source_waiting_time": journey.source_waiting_time, + "source_station_departure_time": journey.source_station_departure_secs, + "source_station_departure_day": self._day_to_str(journey.source_station_departure_day), + + # Arrival information + "target_station_arrival_time": journey.target_station_arrival_secs, + "target_station_arrival_day": self._day_to_str(journey.target_station_arrival_day), + "target_transfer_time": journey.target_transfer_time, + + # Journey steps + "steps": [] + } + + if not detailed: + return journey_dict + + # Convert each journey step + for i in range(journey.steps.size()): + step_dict = self._convert_journey_step(journey.steps[i]) + journey_dict["steps"].append(step_dict) + + return journey_dict + + cdef str _day_to_str(self, Day day): + """Convert Day enum to string""" + if day == Day.CurrentDay: + return "current_day" + else: + return "next_day" + + cdef _convert_journey_step(self, JourneyStep step): + """Convert a JourneyStep to Python dictionary""" + + cdef string stop_id_str = string(b"stop_id") + + step_dict = { + # Basic information + "duration": step.duration, + + # Time information + "departure_time": step.departure_secs, + "arrival_time": step.arrival_secs, + "day": self._day_to_str(step.day), + + # Stop information + "from_stop_id": step.src_stop.getField(stop_id_str).decode('utf-8'), + "to_stop_id": step.dest_stop.getField(stop_id_str).decode('utf-8'), + } + + # Handle optional fields + if step.trip_id.has_value(): + step_dict["trip_id"] = step.trip_id.value().decode('utf-8') + else: + step_dict["trip_id"] = "walking" + + if step.agency_name.has_value(): + step_dict["agency_name"] = step.agency_name.value().decode('utf-8') + else: + step_dict["agency_name"] = "Unknown" + + return step_dict \ No newline at end of file diff --git a/src/routing/pt/cpp_raptor_router/Raptor.cpp b/src/routing/pt/cpp_raptor_router/Raptor.cpp new file mode 100644 index 00000000..c9ff4963 --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/Raptor.cpp @@ -0,0 +1,570 @@ +/** + * @file Raptor.cpp + * @brief Raptor class implementation + * + * This file contains the implementation of the Raptor class, which represents + * the Round-Based Public Transit Routing algorithm, for journey planning. + * + * @author Maria + * @date 10/28/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ + +#include "Raptor.h" + +Raptor::Raptor(const std::unordered_map &agencies, + const std::unordered_map &services, + const std::unordered_map &stops, + const std::unordered_map, Route, pair_hash> &routes, + const std::unordered_map &trips) + : agencies_(agencies), services_(services), stops_(stops), routes_(routes), trips_(trips) { + k = 1; +} + +void Raptor::setQuery(const Query &query) { + query_ = query; + + max_transfers_ = (query_.max_transfers < 0) ? std::numeric_limits::max() : query_.max_transfers; + + included_source_ids_.clear(); + included_target_ids_.clear(); + + for (const auto &source : query_.included_sources) { + // Only include stops that are in stops_ + if (stops_.find(source.first) != stops_.end()) { + included_source_ids_.insert(source.first); + } + } + + for (const auto &target : query_.included_targets) { + // Only include stops that are in stops_ + if (stops_.find(target.first) != stops_.end()) { + included_target_ids_.insert(target.first); + } + } + + if (included_source_ids_.empty() || included_target_ids_.empty()) { + throw std::runtime_error("No included source or target stops found"); + } +} + +void Raptor::initializeAlgorithm() { + // Initialize data structures + arrivals_.clear(); + prev_marked_stops.clear(); + marked_stops.clear(); + + // Initialize arrival times for all stops for each k + StopInfo default_info = {std::nullopt, std::nullopt, std::nullopt, std::nullopt}; + arrivals_.reserve(stops_.size()); + for (const auto &[stop_id, stop]: stops_) { + arrivals_.emplace(stop_id, std::vector(max_transfers_+2, default_info)); // +2 for k=0 and k=max_transfers_+1 + } + + // Initialize the round 0 + k = 0; + int departure_time = Utils::timeToSeconds(query_.departure_time); + // Mark all included source stops that are in included_source_ids_ + for (const auto &source : query_.included_sources) { + if (included_source_ids_.find(source.first) == included_source_ids_.end()) continue; + int station_stop_transfer_time = source.second; + int arrival_time = departure_time + station_stop_transfer_time; + markStop(source.first, arrival_time, std::nullopt, std::nullopt); + } + + k++; // k=1 + + // Fill active trips for current and next day + fillActiveTrips(Day::CurrentDay); + fillActiveTrips(Day::NextDay); +} + +void Raptor::markStop( + const std::string &stop_id, + int arrival, + const std::optional &parent_trip_id, + const std::optional &parent_stop_id +) { + Day day = arrival > MIDNIGHT ? Day::NextDay : Day::CurrentDay; + setMinArrivalTime(stop_id, {arrival, parent_trip_id, parent_stop_id, day}); + marked_stops.insert(stop_id); +} + +void Raptor::setMinArrivalTime( + const std::string &stop_id, + StopInfo stop_info + ) { + arrivals_[stop_id][k] = std::move(stop_info); // Only keep one stop info per stop per round +} + +void Raptor::fillActiveTrips(Day day) { + Date target_date = (day == Day::CurrentDay) ? query_.date : Utils::addOneDay(query_.date); + int target_date_int = Utils::dateToInt(target_date); + + // Check if the active_trips_by_day_ map already has the trips for the target date + if (current_date_int_ == target_date_int) { + return; + } + + // Iterates over all trips + for (auto &[trip_id, trip]: trips_) { + // Get service calendar for the trip + const Service &service = services_.at(trip.getField("service_id")); + + if (service.isActive(target_date)) { + trip.setActive(day, true); + } else { + trip.setActive(day, false); + } + } +} + +std::vector Raptor::findJourneys() { + + std::vector journeys; + + initializeAlgorithm(); + + while (true) { + // std::cout << std::endl << "Round " << k << std::endl; + + // Use the minimum arrival time from the previous round as the base for the current round + setUpperBound(); + + prev_marked_stops = marked_stops; + marked_stops.clear(); + + // Accumulate routes serving marked stops from previous round --> routes_stops_set: ((route_id, direction_id), stop_id) + std::unordered_set, std::string>, nested_pair_hash> routes_stops_set = accumulateRoutesServingStops(); + // std::cout << "Accumulated " << routes_stops_set.size() << " routes serving stops." << std::endl; + + // Traverse each route --> find earliest trip on each route --> traverse trip --> update arrival times + traverseRoutes(routes_stops_set); + // std::cout << "Traversed routes. " << marked_stops.size() << " stop(s) improved." << std::endl; + + // Look for footpaths --> find possible walking connections of marked stops --> update arrival times + handleFootpaths(); + // std::cout << "Handled footpaths. " << marked_stops.size() << " stop(s) improved." << std::endl; + + // Stopping criterion: if no stops are marked, then stop + if (marked_stops.empty()) break; + + // Check if any included target stop has been improved + for (const auto& [target_id, station_stop_transfer_time] : query_.included_targets) { + if (marked_stops.find(target_id) != marked_stops.end()) { + Journey journey = reconstructJourney(target_id, station_stop_transfer_time); + + if (isValidJourney(journey)) { + journeys.push_back(journey); + } + } + } + + // Check if the maximum number of transfers is reached + if (k > max_transfers_) { + // std::cout << "Reached maximum number of transfers (" << max_transfers_ << "). Stopping searching." << std::endl; + break; + } + + k++; + } + return journeys; +} + +std::optional Raptor::findOptimalJourney() { + Journey optimal_journey; + int optimal_target_station_arrival_secs = std::numeric_limits::max(); + + initializeAlgorithm(); + + while (true) { + // std::cout << std::endl << "Round " << k << std::endl; + + // Use the minimum arrival time from the previous round as the base for the current round + setUpperBound(); + + prev_marked_stops = marked_stops; + marked_stops.clear(); + + // Accumulate routes serving marked stops from previous round --> routes_stops_set: ((route_id, direction_id), stop_id) + std::unordered_set, std::string>, nested_pair_hash> routes_stops_set = accumulateRoutesServingStops(); + // std::cout << "Accumulated " << routes_stops_set.size() << " routes serving stops." << std::endl; + + // Traverse each route --> find earliest trip on each route --> traverse trip --> update arrival times + traverseRoutes(routes_stops_set); + // std::cout << "Traversed routes. " << marked_stops.size() << " stop(s) improved." << std::endl; + + // Look for footpaths --> find possible walking connections of marked stops --> update arrival times + handleFootpaths(); + // std::cout << "Handled footpaths. " << marked_stops.size() << " stop(s) improved." << std::endl; + + // Stopping criterion: if no stops are marked, then stop + if (marked_stops.empty()) break; + + for (const auto& [target_id, target_station_stop_transfer_time] : query_.included_targets) { + if (marked_stops.find(target_id) != marked_stops.end()) { + int target_station_arrival_secs = arrivals_[target_id][k].arrival_seconds.value() + target_station_stop_transfer_time; + // TODO: add more criteria for optimality if needed, such as number of transfers, walking time, etc. + if (target_station_arrival_secs < optimal_target_station_arrival_secs) { + Journey possible_journey = reconstructJourney(target_id, target_station_stop_transfer_time); + if (isValidJourney(possible_journey)) { + optimal_target_station_arrival_secs = target_station_arrival_secs; + optimal_journey = possible_journey; + } + } + } + } + + // Check if the maximum number of transfers is reached + if (k > max_transfers_) { + // std::cout << "Reached maximum number of transfers (" << max_transfers_ << "). Stopping searching." << std::endl; + break; + } + + k++; + } + return optimal_journey; +} + +void Raptor::setUpperBound() { + for (const auto &[stop_id, stop]: stops_) + setMinArrivalTime(stop_id, arrivals_[stop_id][k - 1]); +} + +std::unordered_set, std::string>, nested_pair_hash> +Raptor::accumulateRoutesServingStops() { + std::unordered_set, std::string>, nested_pair_hash> routes_stops_set; + + // For each previously marked stop p + for (const auto &marked_stop_id: prev_marked_stops) { + // If the stop is one of the included target stops, skip it + if (included_target_ids_.find(marked_stop_id) != included_target_ids_.end()) continue; + + // For each route r serving p: Route key: (route_id, direction_id) + for (const auto &route_key: stops_[marked_stop_id].getRouteKeys()) { + routes_stops_set.insert({route_key, marked_stop_id}); + } + } + return routes_stops_set; +} + +void Raptor::traverseRoutes( + std::unordered_set, std::string>, nested_pair_hash> routes_stops_set +) { + while (!routes_stops_set.empty()) { + auto route_stop = routes_stops_set.begin(); + const auto &[route_key, p_stop_id] = *route_stop; + + try { + auto et = findEarliestTrip(p_stop_id, route_key); + + if (et.has_value()) { + try { + std::string et_id = et.value().first; + Day et_day = et.value().second; + traverseTrip(et_id, et_day, p_stop_id); + } catch (const std::exception& e) { + std::cerr << "Exception while processing trip: " << e.what() << std::endl; + } + } + } catch (const std::exception& e) { + std::cerr << "Exception in findEarliestTrip for stop " << p_stop_id << " and route " << route_key.first << ": " << e.what() << std::endl; + } + + routes_stops_set.erase(route_stop); + } +} + +std::optional> Raptor::findEarliestTrip( + const std::string &p_stop_id, + const std::pair &route_key +) { + // Get all trips of the route + const auto& route = routes_.at(route_key); + const auto& trips = route.getTripsIds(); + + // Get earliest trip on current day + for (const auto& trip_id: trips) { + if (trips_[trip_id].isActive(Day::CurrentDay)) { + // Get departure time of the trip at p_stop_id + const auto& stop_time_record = trips_[trip_id].getStopTimeRecord(p_stop_id); + if (!stop_time_record) continue; + const int& departure_seconds = stop_time_record->departure_seconds; + if (isValidTrip(p_stop_id, departure_seconds)) + return std::make_pair(trip_id, Day::CurrentDay); + } + } + + // Get earliest trip on next day + for (const auto& trip_id: trips) { + if (trips_[trip_id].isActive(Day::NextDay)) { + // Get departure time of the trip at p_stop_id + const auto& stop_time_record = trips_[trip_id].getStopTimeRecord(p_stop_id); + if (!stop_time_record) continue; + const int& departure_seconds = stop_time_record->departure_seconds + MIDNIGHT; + if (isValidTrip(p_stop_id, departure_seconds)) + return std::make_pair(trip_id, Day::NextDay); + } + } + + return std::nullopt; +} + +bool Raptor::isValidTrip( + const std::string &p_stop_id, + const int &departure_seconds +) { + std::optional stop_prev_arrival_seconds = arrivals_[p_stop_id][k - 1].arrival_seconds; + + if (earlier(departure_seconds, stop_prev_arrival_seconds)) return false; + + // Check if the target stop's arrival time is earlier than the departure time + for (const auto &target_id : included_target_ids_) { + std::optional target_arrival_seconds = arrivals_[target_id][k].arrival_seconds; + if (earlier(departure_seconds, target_arrival_seconds)) + return true; + } + + return false; +} + +void Raptor::traverseTrip( + const std::string &et_id, + const Day &et_day, + const std::string &p_stop_id +) { + Trip et = trips_[et_id]; + + // Find all stop time records of the trip after p_stop_id + const auto& stop_time_records = et.getStopTimeRecordsAfter(p_stop_id); + + // Traverse remaining stops on the trip to update arrival times + for (const auto& stop_time_record: stop_time_records) { + const auto& next_stop_arrival_seconds = stop_time_record.arrival_seconds; + + // Access arrival seconds at next_stop_id for trip et_id, according to the day + int arr_secs = et_day == Day::CurrentDay ? next_stop_arrival_seconds + : next_stop_arrival_seconds + MIDNIGHT; + + // If arrival time can be improved, update Tk(pj) using et + if (improvesArrivalTime(arr_secs, stop_time_record.stop_id)) + markStop(stop_time_record.stop_id, arr_secs, et_id, p_stop_id); + } +} + +bool Raptor::earlier( + int secondsA, + std::optional secondsB +) { + if (!secondsB.has_value()) return true; // if still not set, then any value is better + return secondsA < secondsB.value(); +} + +bool Raptor::improvesArrivalTime( + int arrival, + const std::string &dest_id +) { + // Check if the arrival time at the destination stop can be improved + if (!earlier(arrival, arrivals_[dest_id][k].arrival_seconds)) + return false; + + // Check if the arrival time at any included target stop can be improved + for (const auto &target_id : included_target_ids_) { + if (earlier(arrival, arrivals_[target_id][k].arrival_seconds)) + return true; + } + // If no improvement was found for any target, return false + return false; +} + +void Raptor::handleFootpaths() { + // Copy the marked stops + auto current_marked_stops = marked_stops; + + // For each stop p that is marked by methods traverseTrip() + while (!current_marked_stops.empty()) { + auto it = current_marked_stops.begin(); + std::string stop_id = *it; + current_marked_stops.erase(it); + + try { + // If parent step in the previous round is a footpath, skip it to avoid chaining footpaths + if (isFootpath(arrivals_[stop_id][k - 1])) continue; + + // If parent step in the current round is from a footpath, skip it to avoid chaining footpaths + if (isFootpath(arrivals_[stop_id][k])) continue; + + // If the stop is one of the included target stops, skip it + if (included_target_ids_.find(stop_id) != included_target_ids_.end()) continue; + + // Get the arrival time at the marked stop in this round + int p_arrival = arrivals_[stop_id][k].arrival_seconds.value(); + + // For each footpath (p, p') + for (const auto &[dest_id, duration]: stops_[stop_id].getFootpaths()) { + // DRT Constraint: Skip if destination is a target stop (prevents walking last leg) + if (included_target_ids_.find(dest_id) != included_target_ids_.end()) continue; + + // DRT Constraint: Skip if destination is a source stop (prevents walking first leg) + if (included_source_ids_.find(dest_id) != included_source_ids_.end()) continue; + + int new_arrival = p_arrival + duration; + if (improvesArrivalTime(new_arrival, dest_id)) { + markStop(dest_id, new_arrival, std::nullopt, stop_id); + } + } // end each footpath (p, p') + } catch (const std::exception& e) { + std::cerr << "Exception in handleFootpaths for stop " << stop_id << ": " << e.what() << std::endl; + } + } // end each marked stop p +} + +bool Raptor::isFootpath(const StopInfo &stop_info) { + return stop_info.parent_stop_id.has_value() && !stop_info.parent_trip_id.has_value(); +} + +Journey Raptor::reconstructJourney( + const std::string &target_id, + const int target_station_stop_transfer_time + ) { + Journey journey; + std::string current_stop_id = target_id; + + try { + while (true) { + const std::optional parent_trip_id_opt = arrivals_[current_stop_id][k].parent_trip_id; + // TODO: add agency info into journey step + const std::optional parent_agency_name_opt = std::nullopt; + + const std::optional parent_stop_id_opt = arrivals_[current_stop_id][k].parent_stop_id; + + if (!parent_stop_id_opt.has_value()) break; // No parent stop means it is a source stop + const std::string &parent_stop_id = parent_stop_id_opt.value(); + + int departure_seconds, duration, arrival_seconds; + if (!parent_trip_id_opt.has_value()) { // Walking leg + arrival_seconds = arrivals_[current_stop_id][k].arrival_seconds.value(); + + const auto& footpaths = stops_[parent_stop_id].getFootpaths(); + if (footpaths.find(current_stop_id) != footpaths.end()) { + duration = footpaths.at(current_stop_id); + } else { + duration = 0; // Should not happen + } + departure_seconds = arrival_seconds - duration; + } else { // PT leg + const std::string &parent_trip_id = parent_trip_id_opt.value(); + + std::string route_id = trips_[parent_trip_id].getField("route_id"); + + bool found_route = false; + for (const auto &[key, route]: routes_) { + if (key.first == route_id) { + found_route = true; + break; + } + } + + if (!found_route) { + break; + } + + // Get departure time of the trip at parent_stop_id + const auto& stop_time_record = trips_[parent_trip_id].getStopTimeRecord(parent_stop_id); + departure_seconds = stop_time_record->departure_seconds; + + arrival_seconds = arrivals_[current_stop_id][k].arrival_seconds.value(); + duration = arrival_seconds - departure_seconds; + } + + Day day = arrival_seconds > MIDNIGHT ? Day::NextDay : Day::CurrentDay; + JourneyStep step = {parent_trip_id_opt, parent_agency_name_opt, &stops_[parent_stop_id], &stops_[current_stop_id], + departure_seconds, day, duration, arrival_seconds}; + + journey.steps.push_back(step); + + // Update to the previous stop boarded + current_stop_id = parent_stop_id; + } + + // Reverse the journey to obtain the correct sequence + std::reverse(journey.steps.begin(), journey.steps.end()); + + // If no steps are found, return an empty journey + if (journey.steps.empty()) { + return journey; + } + + // Set journey departure time and day + journey.source_station_departure_secs = Utils::timeToSeconds(query_.departure_time); + journey.source_station_departure_day = Day::CurrentDay; + + // Set journey arrival time and day + journey.target_station_arrival_secs = journey.steps.back().arrival_secs + target_station_stop_transfer_time; + // Update arrival day based on arrival seconds + if (journey.target_station_arrival_secs > MIDNIGHT) { + journey.target_station_arrival_day = Day::NextDay; + } else { + journey.target_station_arrival_day = Day::CurrentDay; + } + // Set journey duration, source transfer time, waiting time, trip time and transfer numbers + journey.duration = journey.target_station_arrival_secs - journey.source_station_departure_secs; + + // Get station stop transfer time from Query + journey.source_transfer_time = 0; + Stop src_stop = *journey.steps.front().src_stop; + for (const auto &source : query_.included_sources) { + if (source.first == src_stop.getField("stop_id")) { + journey.source_transfer_time = source.second; + break; + } + } + + journey.target_transfer_time = target_station_stop_transfer_time; + + journey.source_waiting_time = journey.steps.front().departure_secs - journey.source_station_departure_secs - journey.source_transfer_time; + if (journey.source_waiting_time < 0) journey.source_waiting_time = 0; + + journey.trip_time = journey.duration - journey.source_waiting_time - journey.target_transfer_time - journey.source_transfer_time; + + int vehicle_legs = 0; + for (const auto& step : journey.steps) { + if (step.trip_id.has_value() && step.trip_id.value() != "walking") { + vehicle_legs++; + } + } + journey.num_transfers = (vehicle_legs > 0) ? (vehicle_legs - 1) : 0; + } catch (const std::exception& e) { + std::cerr << "Exception in reconstructJourney: " << e.what() << std::endl; + } + + return journey; +} + +bool Raptor::isValidJourney(Journey journey) const { + if (journey.steps.empty()) + return false; + + // Get the starting stop ID of the journey + std::string start_stop_id = journey.steps.front().src_stop->getField("stop_id"); + + // Check if the starting stop is one of the included source IDs + if (included_source_ids_.find(start_stop_id) == included_source_ids_.end()) { + return false; + } + + // DRT Constraint: First leg must be a vehicle trip (not walking) + if (!journey.steps.front().trip_id.has_value()) { + return false; + } + + // DRT Constraint: Last leg must be a vehicle trip (not walking) + if (!journey.steps.back().trip_id.has_value()) { + return false; + } + + return true; +} \ No newline at end of file diff --git a/src/routing/pt/cpp_raptor_router/Raptor.h b/src/routing/pt/cpp_raptor_router/Raptor.h new file mode 100644 index 00000000..b26a8e6e --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/Raptor.h @@ -0,0 +1,272 @@ +/** + * @file Raptor.h + * @brief Defines the Raptor class for finding Pareto-optimal journeys in a transit network. + * + * This header file declares the Raptor class, which implements the + * Round-Based Public Transit Routing algorithm. + * + * The main method involve finding journeys. + * + * The class also contains several private methods for initializing the algorithm, + * traversing routes, and reconstructing journeys. + * + * @author Maria + * @date 10/28/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ + +#ifndef RAPTOR_RAPTOR_H +#define RAPTOR_RAPTOR_H + +#include +#include +#include // for setw +#include "Parser.h" +#include "Utils.h" + +/** + * @class Raptor + * @brief Implements the RAPTOR algorithm for finding Pareto-optimal journeys. + * + * The Raptor class provides methods to set a query, find Pareto-optimal journeys, + * and print journey steps. It uses various data structures to store information + * about agencies, calendars, stops, routes, trips, and stop times. + */ +class Raptor { +public: + + /** + * @brief Default constructor for the Raptor class. + * + * Initializes the Raptor object with empty data structures. + */ + Raptor() = default; + + /** + * @brief Parameterized constructor for Raptor. + * + * Initializes the Raptor object with provided agency, calendar, stop, route, trip, and stop time data. + * + * @param[in] agencies_ A map of agency IDs to Agency objects. + * @param[in] services_ A map of service IDs to Service objects. + * @param[in] stops A map of stop IDs to Stop objects. + * @param[in] routes A map of pairs of route IDs and direction IDs to Route objects. + * @param[in] trips A map of trip IDs to Trip objects. + */ + Raptor(const std::unordered_map &agencies_, + const std::unordered_map &services_, + const std::unordered_map &stops, + const std::unordered_map, Route, pair_hash> &routes, + const std::unordered_map &trips); + + /** + * @brief Sets the query for the Raptor algorithm. + * + * @param[in] query The query containing the parameters for journey search. + */ + void setQuery(const Query &query); + + /** + * @brief Finds all Pareto-optimal journeys. + * + * This function uses the RAPTOR algorithm to compute all optimal journeys based on the provided query. + * + * @return A vector of Journey objects representing the Pareto-optimal journeys. + */ + std::vector findJourneys(); + + /** + * @brief Finds the journey with the earliest arrival time. + * + * @return A Journey object representing the journey with the earliest arrival time. + */ + std::optional findOptimalJourney(); + + /** + * @brief Validates if the given journey is valid. + * + * Checks whether the given journey meets the required criteria. + * + * @param[in] journey The Journey object to be validated. + * @return True if the journey is valid, false otherwise. + */ + bool isValidJourney(Journey journey) const; + +private: + + std::unordered_map agencies_; ///< Map of agency IDs to Agency objects. + std::unordered_map services_; ///< Map of service IDs to Service objects. + std::unordered_map stops_; ///< Map of stop IDs to Stop objects. + std::unordered_map, Route, pair_hash> routes_; ///< Map of route keys to Route objects. + std::unordered_map trips_; ///< Map of trip IDs to Trip objects. + int current_date_int_; ///< The current date as an integer. + + Query query_; ///< The current query for the RAPTOR algorithm. + int max_transfers_; ///< The maximum number of transfers allowed in a journey. + std::unordered_set included_source_ids_; ///< Set of included source stop IDs. + std::unordered_set included_target_ids_; ///< Set of included target stop IDs. + std::unordered_map> arrivals_; ///< Map of stop IDs to vectors of StopInfo for each k. + std::unordered_set prev_marked_stops; ///< Set of previously marked stops. + std::unordered_set marked_stops; ///< Set of currently marked stops. + int k{}; ///< The current round of the algorithm. + + /** + * @brief Initializes the algorithm by setting required parameters. + */ + void initializeAlgorithm(); + + // TODO: Remove this function + /** + * @brief Sets the minimum arrival time for a given stop. + * + * @param[in] stop_id The ID of the stop. + * @param[in] stop_info The stop info containing the arrival time, parent trip ID, and parent stop ID. + * + * @deprecated This function is deprecated and will be removed in the future. + */ + void setMinArrivalTime(const std::string &stop_id, StopInfo stop_info); + + /** + * @brief Fills the active trips for a given day. + * + * @param[in] day The day for which trips are being filled. + */ + void fillActiveTrips(Day day); + + /** + * @brief Sets the upper bound for the search, based on previous round. + */ + void setUpperBound(); + + /** + * @brief Accumulates routes serving each stop. + * + * @return A set of routes that serve stops. + */ + std::unordered_set, std::string>, nested_pair_hash> + accumulateRoutesServingStops(); + + /** + * @brief Traverses the routes serving each stop. + * + * @param[in] routes_stops_set The set of routes and stops to be traversed. + */ + void traverseRoutes( + std::unordered_set, std::string>, nested_pair_hash> routes_stops_set + ); + + /** + * @brief Finds the earliest trip for a given stop and route. + * + * @param[in] pi_stop_id The ID of the stop. + * @param[in] route_key The key consisting of route and direction. + * @return An optional pair of trip ID and day if found. + */ + std::optional> findEarliestTrip( + const std::string &pi_stop_id, + const std::pair &route_key + ); + + /** + * @brief Checks if a trip is valid based on the route and stop time. + * + * @param[in] p_stop_id The stop ID. + * @param[in] departure_seconds The departure time in seconds. + * @return True if the trip is valid, false otherwise. + */ + bool isValidTrip( + const std::string &p_stop_id, + const int &departure_seconds + ); + + /** + * @brief Checks if the service is active based on the calendar and date. + * + * @param[in] service The service object containing service dates. + * @param[in] date The date to check. + * @return True if the service is active on the given date, false otherwise. + */ + static bool isServiceActive(const Service &service, const Date &date); + + /** + * @brief Traverses a specific trip. + * + * @param[in] et_id The trip ID. + * @param[in] et_day The day of travel. + * @param[in] p_stop_id The stop ID for the trip. + */ + void traverseTrip( + const std::string &et_id, + const Day &et_day, + const std::string &p_stop_id + ); + + /** + * @brief Compares two arrival times to determine which is earlier. + * + * @param[in] secondsA The first arrival time in seconds. + * @param[in] secondsB The second arrival time in seconds. + * @return True if the first arrival time is earlier, false otherwise. + */ + static bool earlier(int secondsA, std::optional secondsB); + + /** + * @brief Checks if a step improves the arrival time for a destination. + * + * @param[in] arrival The arrival time. + * @param[in] dest_id The destination stop ID. + * @return True if the arrival time improves, false otherwise. + */ + bool improvesArrivalTime(int arrival, const std::string &dest_id); + + /** + * @brief Marks a stop with the arrival time, parent trip, and parent stop. + * + * @param[in] stop_id The ID of the stop. + * @param[in] arrival The arrival time at the stop. + * @param[in] parent_trip_id The ID of the parent trip. + * @param[in] parent_stop_id The ID of the parent stop. + */ + void markStop( + const std::string &stop_id, + int arrival, + const std::optional &parent_trip_id, + const std::optional &parent_stop_id + ); + + // TODO: Remove this function + /** + * @brief Handles footpath logic during traversal. + * + * @deprecated This function is deprecated and will be removed in the future. + */ + void handleFootpaths(); + + // TODO: Remove this function + /** + * @brief Checks if the given stop info represents a footpath. + * + * @param[in] stop_info The stop info to be checked. + * @return True if the stop is a footpath, false otherwise. + * + * @deprecated This function is deprecated and will be removed in the future. + */ + static bool isFootpath(const StopInfo &stop_info); + + /** + * @brief Reconstructs the journey from a given stop. + * + * @param[in] target_id The ID of the target stop for the journey. + * @param[in] station_stop_transfer_time The target station stop transfer time for the journey. + * @return A Journey object representing the reconstructed journey. + */ + Journey reconstructJourney( + const std::string &target_id, + const int station_stop_transfer_time + ); +}; + +#endif //RAPTOR_RAPTOR_H diff --git a/src/routing/pt/cpp_raptor_router/Utils.cpp b/src/routing/pt/cpp_raptor_router/Utils.cpp new file mode 100644 index 00000000..95963ec2 --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/Utils.cpp @@ -0,0 +1,130 @@ +/** + * @file Utils.cpp + * @brief Provides utility functions for the RAPTOR application. + * + * This file contains utility functions for the RAPTOR application, + * including functions for calculating distances, durations, and time conversions. + * + * @author Maria + * @date 10/28/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ + +#include "Utils.h" + +std::string Utils::secondsToTime(std::optional seconds) { + + if (!seconds.has_value()) return "INF"; + + int seconds_value = seconds.value(); + + int hours = seconds_value / 3600 % 24; + int minutes = (seconds_value % 3600) / 60; + int secs = seconds_value % 60; + + std::ostringstream oss; + oss << std::setw(2) << std::setfill('0') << hours << ":" + << std::setw(2) << std::setfill('0') << minutes << ":" + << std::setw(2) << std::setfill('0') << secs; + return oss.str(); +} + +int Utils::timeToSeconds(const std::string &timeStr) { + int hours, minutes, seconds; + char colon; + + std::istringstream iss(timeStr); + iss >> hours >> colon >> minutes >> colon >> seconds; + + return hours * 3600 + minutes * 60 + seconds; +} + +int Utils::timeToSeconds(const Time &time) { + return time.hours * 3600 + time.minutes * 60 + time.seconds; +} + +int Utils::dateToInt(const Date &date) { + return date.year * 10000 + date.month * 100 + date.day; +} + +std::vector Utils::split(const std::string &str, char delimiter) { + std::vector tokens; + std::string token; + std::istringstream tokenStream(str); + + while (std::getline(tokenStream, token, delimiter)) { + tokens.push_back(token); + } + + if (str.back() == delimiter) { + tokens.emplace_back(""); // Add empty token if the last character is the delimiter + } + + return tokens; +} + + +std::string Utils::getFirstWord(const std::string &str) { + return str.substr(0, str.find(' ')); +} + +void Utils::clean(std::string &input) { + size_t first = input.find_first_not_of(" \t\n\r"); + size_t last = input.find_last_not_of(" \t\n\r"); + input = (first == std::string::npos) ? "" : input.substr(first, (last - first + 1)); +} + + +bool Utils::isNumber(const std::string &str) { + return !str.empty() && std::all_of(str.begin(), str.end(), [](char c) { return std::isdigit(c); }); +} + +int Utils::daysInMonth(int year, int month) { + static const int daysInMonths[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) { + return 29; // February in a leap year + } + return daysInMonths[month - 1]; +} + +bool Utils::dateWithinRange(const Date &date, const std::string &start_date, const std::string &end_date) { + // Convert strings YYYYMMDD to Date objects + Date start = {std::stoi(start_date.substr(0, 4)), std::stoi(start_date.substr(4, 2)), + std::stoi(start_date.substr(6, 2))}; + Date end = {std::stoi(end_date.substr(0, 4)), std::stoi(end_date.substr(4, 2)), std::stoi(end_date.substr(6, 2))}; + + // Check if the date is earlier than the start date + if ((date.year < start.year) + || (date.year == start.year && date.month < start.month) + || (date.year == start.year && date.month == start.month && date.day < start.day)) + return false; + + // Check if the date is later than the end date + if ((date.year > end.year) + || (date.year == end.year && date.month > end.month) + || (date.year == end.year && date.month == end.month && date.day > end.day)) + return false; + + return true; +} + +Date Utils::addOneDay(Date date) { + std::tm time_info = {}; + time_info.tm_year = date.year - 1900; + time_info.tm_mon = date.month - 1; + time_info.tm_mday = date.day + 1; // Add one day + int date_weekday = (time_info.tm_wday == 0) ? 6 : time_info.tm_wday - 1; // 0-6, 0 = Monday, 6 = Sunday + + std::mktime(&time_info); // Normalize the date + Date new_date = {time_info.tm_year + 1900, time_info.tm_mon + 1, time_info.tm_mday, date_weekday}; + + return new_date; // Get the next day +} + +std::string Utils::dayToString(Day day) { + return (day == Day::CurrentDay) ? "current" : "next"; +} + diff --git a/src/routing/pt/cpp_raptor_router/Utils.h b/src/routing/pt/cpp_raptor_router/Utils.h new file mode 100644 index 00000000..4dd81c4b --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/Utils.h @@ -0,0 +1,164 @@ +/** + * @file Utils.h + * @brief Provides utility functions for the RAPTOR application. + * + * This header file declares utility functions for the RAPTOR application, + * including functions for calculating distances, durations, and time conversions. + * + * @author Maria + * @date 10/28/2024 + * + * @version fleetpy-1.0 + * @author Chenhao Ding + * @date 12/12/2025 + */ + +#ifndef RAPTOR_UTILS_H +#define RAPTOR_UTILS_H + +#include +#include +#include +#include // for std::pair +#include +#include + +#include "DateTime.h" + +/** + * @class Utils + * @brief A utility class providing various helper functions. + * + * This class contains static utility methods to handle mathematical calculations, time conversions, + * string manipulations, and date operations. These methods are used throughout the RAPTOR project + * to simplify code and provide common functionality. + */ +class Utils { +public: + + /** + * @brief Converts a time in seconds to a string format (HH:MM:SS). + * + * This method converts a given time in seconds into a formatted string representing the time + * in the "HH:MM:SS" format. + * + * @param[in] seconds The time in seconds. + * @return A string representation of the time in "HH:MM:SS" format. + */ + static std::string secondsToTime(std::optional seconds); + + /** + * @brief Converts a time string to the equivalent number of seconds. + * + * This method converts a time string (e.g., "12:30:00") to the total number of seconds. + * + * @param[in] timeStr A time string in the "HH:MM:SS" format. + * @return The total time in seconds. + */ + static int timeToSeconds(const std::string &timeStr); + + /** + * @brief Converts a Time object to the equivalent number of seconds. + * + * This method converts a Time object to the total number of seconds since midnight. + * + * @param[in] time A Time object representing a specific time. + * @return The total time in seconds. + */ + static int timeToSeconds(const Time &time); + + /** + * @brief Converts a date to an integer format (YYYYMMDD). + * + * This method converts a Date object to an integer representation in the format "YYYYMMDD". + * + * @param[in] date The Date object to be converted. + * @return An integer representation of the date in the format "YYYYMMDD". + */ + static int dateToInt(const Date &date); + + /** + * @brief Splits a string into a vector of substrings based on a delimiter. + * + * This method splits a string into parts wherever a specified delimiter appears. + * + * @param[in] str The input string to be split. + * @param[in] delimiter The delimiter character to split the string by. + * @return A vector of substrings split from the input string. + */ + static std::vector split(const std::string &str, char delimiter); + + /** + * @brief Retrieves the first word in a string. + * + * This method extracts and returns the first word from a given string, stopping at the first space. + * + * @param[in] str The input string. + * @return The first word in the string. + */ + static std::string getFirstWord(const std::string &str); + + /** + * @brief Trims leading and trailing whitespace from a string. + * + * This method removes any leading or trailing whitespace from the given string. + * + * @param[in,out] line The line to be cleaned. + */ + static void clean(std::string &input); + + /** + * @brief Checks if a string represents a valid number. + * + * This method checks whether the input string can be interpreted as a valid numerical value. + * + * @param[in] str The input string to be checked. + * @return True if the string is a valid number, false otherwise. + */ + static bool isNumber(const std::string &str); + + /** + * @brief Retrieves the number of days in a specific month of a specific year. + * + * This method returns the number of days in a given month, accounting for leap years if applicable. + * + * @param[in] year The year of interest. + * @param[in] month The month of interest (1-12). + * @return The number of days in the specified month of the specified year. + */ + static int daysInMonth(int year, int month); + + /** + * @brief Checks if a date is within a specified date range. + * + * This method checks whether a given date falls within the specified range of start and end dates. + * + * @param[in] date The date to be checked. + * @param[in] start_date The start of the date range (in string format). + * @param[in] end_date The end of the date range (in string format). + * @return True if the date is within the range, false otherwise. + */ + static bool dateWithinRange(const Date &date, const std::string &start_date, const std::string &end_date); + + /** + * @brief Adds one day to a given date. + * + * This method increments the given date by one day. + * + * @param[in] date The date to which one day should be added. + * @return The resulting date after adding one day. + */ + static Date addOneDay(Date date); + + /** + * @brief Converts a Day enum to a string representation. + * + * This method converts a Day enum (Current or Next) to its string representation. + * + * @param[in] day The Day enum to be converted. + * @return The string representation of the specified day. + */ + static std::string dayToString(Day day); +}; + +#endif //RAPTOR_UTILS_H diff --git a/src/routing/pt/cpp_raptor_router/setup.py b/src/routing/pt/cpp_raptor_router/setup.py new file mode 100644 index 00000000..fe65848a --- /dev/null +++ b/src/routing/pt/cpp_raptor_router/setup.py @@ -0,0 +1,36 @@ +from setuptools import Extension, setup +from Cython.Build import cythonize +import os +import platform + +# Automatically collect all C++ source files +def get_cpp_sources(): + sources = [] + for root, dirs, files in os.walk("."): + for file in files: + if file.endswith(".cpp") and file != "PyPTRouter.cpp": + sources.append(os.path.join(root, file)) + return sources + +# Create a simple extension module +ext = Extension( + name="PyPTRouter", # Module name for import + sources=[ + "PyPTRouter.pyx", # Cython source file + *get_cpp_sources() # All C++ source files + ], + include_dirs=["."], # Directory containing header files + language="c++", + extra_compile_args=["-std=c++20"] if platform.system() != "Windows" else ["/std:c++20"], +) + +# Configure the setup +setup( + ext_modules=cythonize( + ext, + compiler_directives={ + "language_level": 3, + "embedsignature": True, + }, + ), +) \ No newline at end of file diff --git a/src/routing/NetworkBase.py b/src/routing/road/NetworkBase.py similarity index 100% rename from src/routing/NetworkBase.py rename to src/routing/road/NetworkBase.py diff --git a/src/routing/NetworkBasic.py b/src/routing/road/NetworkBasic.py similarity index 99% rename from src/routing/NetworkBasic.py rename to src/routing/road/NetworkBasic.py index 2f1eeeaf..dfae0693 100644 --- a/src/routing/NetworkBasic.py +++ b/src/routing/road/NetworkBasic.py @@ -23,8 +23,8 @@ # src imports # ----------- -from src.routing.NetworkBase import NetworkBase -from src.routing.routing_imports.Router import Router +from src.routing.road.NetworkBase import NetworkBase +from src.routing.road.routing_imports.Router import Router # -------------------------------------------------------------------------------------------------------------------- # # global variables diff --git a/src/routing/NetworkBasicCpp.py b/src/routing/road/NetworkBasicCpp.py similarity index 99% rename from src/routing/NetworkBasicCpp.py rename to src/routing/road/NetworkBasicCpp.py index 44115335..652de37b 100644 --- a/src/routing/NetworkBasicCpp.py +++ b/src/routing/road/NetworkBasicCpp.py @@ -11,8 +11,8 @@ # src imports # ----------- -from src.routing.NetworkBasic import NetworkBasic -from src.routing.cpp_router.PyNetwork import PyNetwork +from src.routing.road.NetworkBasic import NetworkBasic +from src.routing.road.cpp_router.PyNetwork import PyNetwork # -------------------------------------------------------------------------------------------------------------------- # # global variables diff --git a/src/routing/NetworkBasicWithStore.py b/src/routing/road/NetworkBasicWithStore.py similarity index 98% rename from src/routing/NetworkBasicWithStore.py rename to src/routing/road/NetworkBasicWithStore.py index d6272037..40644f18 100644 --- a/src/routing/NetworkBasicWithStore.py +++ b/src/routing/road/NetworkBasicWithStore.py @@ -22,8 +22,8 @@ # src imports # ----------- -from src.routing.NetworkBasic import NetworkBasic -from src.routing.routing_imports.Router import Router +from src.routing.road.NetworkBasic import NetworkBasic +from src.routing.road.routing_imports.Router import Router # -------------------------------------------------------------------------------------------------------------------- # # global variables diff --git a/src/routing/NetworkBasicWithStoreCpp.py b/src/routing/road/NetworkBasicWithStoreCpp.py similarity index 98% rename from src/routing/NetworkBasicWithStoreCpp.py rename to src/routing/road/NetworkBasicWithStoreCpp.py index fbf08b32..9f70db35 100644 --- a/src/routing/NetworkBasicWithStoreCpp.py +++ b/src/routing/road/NetworkBasicWithStoreCpp.py @@ -22,8 +22,8 @@ # src imports # ----------- -from src.routing.NetworkBasicCpp import NetworkBasicCpp -from src.routing.cpp_router.PyNetwork import PyNetwork +from src.routing.road.NetworkBasicCpp import NetworkBasicCpp +from src.routing.road.cpp_router.PyNetwork import PyNetwork # -------------------------------------------------------------------------------------------------------------------- # # global variables diff --git a/src/routing/NetworkForPreprocessing.py b/src/routing/road/NetworkForPreprocessing.py similarity index 93% rename from src/routing/NetworkForPreprocessing.py rename to src/routing/road/NetworkForPreprocessing.py index dfb54537..9f03f00e 100644 --- a/src/routing/NetworkForPreprocessing.py +++ b/src/routing/road/NetworkForPreprocessing.py @@ -1,4 +1,4 @@ -from src.routing.NetworkBasic import NetworkBasic, Node, Edge +from src.routing.road.NetworkBasic import NetworkBasic, Node, Edge class NetworkForPreprocessing(NetworkBasic): """ this network is only used in network_manipulation.py to evalute connectivity """ diff --git a/src/routing/NetworkImmediatePreproc.py b/src/routing/road/NetworkImmediatePreproc.py similarity index 97% rename from src/routing/NetworkImmediatePreproc.py rename to src/routing/road/NetworkImmediatePreproc.py index 63d21095..2e36b044 100644 --- a/src/routing/NetworkImmediatePreproc.py +++ b/src/routing/road/NetworkImmediatePreproc.py @@ -14,7 +14,7 @@ # ----------------------------- import os import logging -from src.routing.NetworkBasicWithStore import NetworkBasicWithStore +from src.routing.road.NetworkBasicWithStore import NetworkBasicWithStore # -------------------------------------------------------------------------------------------------------------------- # diff --git a/src/routing/NetworkPartialPreprocessed.py b/src/routing/road/NetworkPartialPreprocessed.py similarity index 98% rename from src/routing/NetworkPartialPreprocessed.py rename to src/routing/road/NetworkPartialPreprocessed.py index 2437ea35..102275bd 100644 --- a/src/routing/NetworkPartialPreprocessed.py +++ b/src/routing/road/NetworkPartialPreprocessed.py @@ -22,8 +22,8 @@ # src imports # ----------- -from src.routing.NetworkBasic import NetworkBasic, Node, Edge -from src.routing.routing_imports.Router import Router +from src.routing.road.NetworkBasic import NetworkBasic, Node, Edge +from src.routing.road.routing_imports.Router import Router # -------------------------------------------------------------------------------------------------------------------- # # global variables diff --git a/src/routing/NetworkPartialPreprocessedCpp.py b/src/routing/road/NetworkPartialPreprocessedCpp.py similarity index 99% rename from src/routing/NetworkPartialPreprocessedCpp.py rename to src/routing/road/NetworkPartialPreprocessedCpp.py index 24be61a0..dbb5c2a2 100644 --- a/src/routing/NetworkPartialPreprocessedCpp.py +++ b/src/routing/road/NetworkPartialPreprocessedCpp.py @@ -22,8 +22,8 @@ # src imports # ----------- -from src.routing.NetworkBasicCpp import NetworkBasicCpp -from src.routing.cpp_router.PyNetwork import PyNetwork +from src.routing.road.NetworkBasicCpp import NetworkBasicCpp +from src.routing.road.cpp_router.PyNetwork import PyNetwork # -------------------------------------------------------------------------------------------------------------------- # # global variables diff --git a/src/routing/NetworkTTMatrix.py b/src/routing/road/NetworkTTMatrix.py similarity index 99% rename from src/routing/NetworkTTMatrix.py rename to src/routing/road/NetworkTTMatrix.py index 2996fef9..3d338960 100644 --- a/src/routing/NetworkTTMatrix.py +++ b/src/routing/road/NetworkTTMatrix.py @@ -13,7 +13,7 @@ # src imports # ----------- -from src.routing.NetworkBase import NetworkBase +from src.routing.road.NetworkBase import NetworkBase # -------------------------------------------------------------------------------------------------------------------- # # global variables diff --git a/src/routing/cpp_router/Edge.cpp b/src/routing/road/cpp_router/Edge.cpp similarity index 100% rename from src/routing/cpp_router/Edge.cpp rename to src/routing/road/cpp_router/Edge.cpp diff --git a/src/routing/cpp_router/Edge.h b/src/routing/road/cpp_router/Edge.h similarity index 100% rename from src/routing/cpp_router/Edge.h rename to src/routing/road/cpp_router/Edge.h diff --git a/src/routing/cpp_router/Network.cpp b/src/routing/road/cpp_router/Network.cpp similarity index 100% rename from src/routing/cpp_router/Network.cpp rename to src/routing/road/cpp_router/Network.cpp diff --git a/src/routing/cpp_router/Network.h b/src/routing/road/cpp_router/Network.h similarity index 100% rename from src/routing/cpp_router/Network.h rename to src/routing/road/cpp_router/Network.h diff --git a/src/routing/cpp_router/Network.pxd b/src/routing/road/cpp_router/Network.pxd similarity index 100% rename from src/routing/cpp_router/Network.pxd rename to src/routing/road/cpp_router/Network.pxd diff --git a/src/routing/cpp_router/Node.cpp b/src/routing/road/cpp_router/Node.cpp similarity index 100% rename from src/routing/cpp_router/Node.cpp rename to src/routing/road/cpp_router/Node.cpp diff --git a/src/routing/cpp_router/Node.h b/src/routing/road/cpp_router/Node.h similarity index 100% rename from src/routing/cpp_router/Node.h rename to src/routing/road/cpp_router/Node.h diff --git a/src/routing/cpp_router/PyNetwork.pyx b/src/routing/road/cpp_router/PyNetwork.pyx similarity index 100% rename from src/routing/cpp_router/PyNetwork.pyx rename to src/routing/road/cpp_router/PyNetwork.pyx diff --git a/src/routing/cpp_router/cpp_router_checker.py b/src/routing/road/cpp_router/cpp_router_checker.py similarity index 92% rename from src/routing/cpp_router/cpp_router_checker.py rename to src/routing/road/cpp_router/cpp_router_checker.py index 6cf48311..857562bb 100644 --- a/src/routing/cpp_router/cpp_router_checker.py +++ b/src/routing/road/cpp_router/cpp_router_checker.py @@ -7,8 +7,8 @@ Fleetpy_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) sys.path.append(Fleetpy_dir) -from src.routing.NetworkBasicCpp import NetworkBasicCpp -from src.routing.NetworkBasic import NetworkBasic +from src.routing.road.NetworkBasicCpp import NetworkBasicCpp +from src.routing.road.NetworkBasic import NetworkBasic """ run this script to check if the C++ router returns the same results as the python router diff --git a/src/routing/cpp_router/setup.py b/src/routing/road/cpp_router/setup.py similarity index 100% rename from src/routing/cpp_router/setup.py rename to src/routing/road/cpp_router/setup.py diff --git a/src/routing/routing_imports/PriorityQueue_python3.py b/src/routing/road/routing_imports/PriorityQueue_python3.py similarity index 100% rename from src/routing/routing_imports/PriorityQueue_python3.py rename to src/routing/road/routing_imports/PriorityQueue_python3.py diff --git a/src/routing/routing_imports/Router.py b/src/routing/road/routing_imports/Router.py similarity index 99% rename from src/routing/routing_imports/Router.py rename to src/routing/road/routing_imports/Router.py index a818747c..32fa32ef 100644 --- a/src/routing/routing_imports/Router.py +++ b/src/routing/road/routing_imports/Router.py @@ -5,7 +5,7 @@ from . import PriorityQueue_python3 as PQ except: try: - import src.routing.routing_imports.PriorityQueue_python3 as PQ + import src.routing.road.routing_imports.PriorityQueue_python3 as PQ except: raise ImportError("couldnt import PriorityQueue_python3") diff --git a/src/routing/routing_imports/__init__.py b/src/routing/road/routing_imports/__init__.py similarity index 100% rename from src/routing/routing_imports/__init__.py rename to src/routing/road/routing_imports/__init__.py diff --git a/src/simulation/Offers.py b/src/simulation/Offers.py index 2b47bd35..7b38e02c 100644 --- a/src/simulation/Offers.py +++ b/src/simulation/Offers.py @@ -1,7 +1,8 @@ # src imports # ----------- +import typing as tp -from src.routing.NetworkBase import return_position_str +from src.routing.road.NetworkBase import return_position_str from src.misc.globals import * # -------------------------------------------------------------------------------------------------------------------- # @@ -135,4 +136,166 @@ def __str__(self): class Rejection(TravellerOffer): """This class takes minimal input and creates an offer that represents a rejection.""" def __init__(self, traveler_id, operator_id): - super().__init__(traveler_id, operator_id, offered_waiting_time=None, offered_driving_time=None, fare=None) \ No newline at end of file + super().__init__(traveler_id, operator_id, offered_waiting_time=None, offered_driving_time=None, fare=None) + + +class PTOffer(TravellerOffer): + """This class represents a public transport offer. + + A PT offer contains the following information: + - traveler_id (str): sub-request id struct of the parent request + - operator_id (int): id of PT operator (-2) + - source_station_id (str): id of the source station + - target_station_id (str): id of the target station + - origin_node_arrival_time (int): absolute time [s] of the arrival at the origin street node + - source_walking_time (int): walking time [s] from origin street node to source station + - source_station_departure_time (int): absolute time [s] of the departure at the source station + - source_transfer_time (int): transfer time [s] from the source station to the source stop + - waiting_time (int): waiting time [s] from arrival at the source stop until departure; this value is used as the 'offered_waiting_time' in the TravellerOffer + - trip_time (int): travel time [s] from departure at the source stop until arrival at the target stop + - fare (int): fare of the offer + - target_transfer_time (int): transfer time [s] from the target stop to the target station + - target_station_arrival_time (int): absolute time [s] of the arrival at the target station + - target_walking_time (int): walking time [s] from target station to destination street node + - destination_node_arrival_time (int): absolute time [s] of the arrival at the destination street node + - num_transfers (int): number of transfers in the PT journey + - pt_journey_duration (int): duration [s] from departure at the source station to arrival at the target station + - pt_segment_duration (int): duration [s] from arrival at the origin street node to arrival at the destination street node + - detailed_journey_plan (dict): detailed journey plan (only if requested) + """ + def __init__( + self, + traveler_id: str, operator_id: int, + source_station_id: str, target_station_id: str, + source_walking_time: int, source_station_departure_time: int, source_transfer_time: int, + waiting_time: int, trip_time: int, fare: int, + target_transfer_time: int, target_station_arrival_time: int, target_walking_time: int, + num_transfers: int, pt_journey_duration: int, detailed_journey_plan: tp.List[tp.Dict[str, tp.Any]], + ): + self.origin_node_arrival_time = source_station_departure_time - source_walking_time + self.destination_node_arrival_time = target_station_arrival_time + target_walking_time + + # the latest arrival time at the origin node is the arrival time plus the waiting time buffer + self.origin_node_latest_arrival_time = self.origin_node_arrival_time + waiting_time + + self.pt_segment_duration = self.destination_node_arrival_time - self.origin_node_arrival_time + offered_driving_time = self.pt_segment_duration - waiting_time + + self.detailed_journey_plan = detailed_journey_plan + + additional_parameters = { + G_PT_OFFER_SOURCE_STATION: source_station_id, + G_PT_OFFER_TARGET_STATION: target_station_id, + G_PT_OFFER_SOURCE_WALKING_TIME: source_walking_time, + G_PT_OFFER_SOURCE_STATION_DEPARTURE_TIME: source_station_departure_time, + G_PT_OFFER_SOURCE_TRANSFER_TIME: source_transfer_time, + G_PT_OFFER_TRIP_TIME: trip_time, + G_PT_OFFER_TARGET_TRANSFER_TIME: target_transfer_time, + G_PT_OFFER_TARGET_STATION_ARRIVAL_TIME: target_station_arrival_time, + G_PT_OFFER_TARGET_WALKING_TIME: target_walking_time, + G_PT_OFFER_NUM_TRANSFERS: num_transfers, + G_PT_OFFER_DURATION: pt_journey_duration, + } + + super().__init__(traveler_id, operator_id, waiting_time, offered_driving_time, fare, additional_parameters=additional_parameters) + + +class IntermodalOffer(TravellerOffer): + """This class represents an intermodal offer that consists of multiple segments served by different operators.""" + def __init__( + self, + traveler_id: int, + sub_trip_offers: tp.Dict[int, TravellerOffer], + rq_modal_state: RQ_MODAL_STATE, + ): + """Initialize an intermodal offer that can include multiple sub-trips from different operators. + + :param traveler_id: traveler_id this offer is sent to + :type traveler_id: int + :param sub_trip_offers: dictionary of sub-trip offers {sub_trip_id: TravellerOffer} + :type sub_trip_offers: dict + :param rq_modal_state: modal state of the parent request + :type rq_modal_state: RQ_MODAL_STATE + :param additional_parameters: dictionary of other offer attributes + :type additional_parameters: dict or None + """ + self.rq_modal_state = rq_modal_state + self.sub_trip_offers: tp.Dict[int, TravellerOffer] = sub_trip_offers + + self.additional_offer_parameters: tp.Dict[str, tp.Any] = {} + + # merge sub-trip offers + aggregated_offer: tp.Dict[str, tp.Any] = self._merge_sub_trip_offers() + operator_sub_trip_tuple: tp.Tuple[tp.Tuple[int, int]] = aggregated_offer[G_IM_OFFER_OPERATOR_SUB_TRIP_TUPLE] # ((operator_id, sub_trip_id), ...) + self.operator_sub_trip_tuple_str = self.convert_operator_sub_trip_tuple_to_str(operator_sub_trip_tuple) + offered_waiting_time: int = aggregated_offer[G_OFFER_WAIT] + offered_driving_time: int = aggregated_offer[G_OFFER_DRIVE] + fare: int = aggregated_offer[G_OFFER_FARE] + + super().__init__(traveler_id, operator_sub_trip_tuple, offered_waiting_time, offered_driving_time, fare, self.additional_offer_parameters) + + def get_sub_trip_offers(self) -> tp.Dict[int, TravellerOffer]: + """Get the sub-trip offers for the multimodal offer.""" + return self.sub_trip_offers + + def convert_operator_sub_trip_tuple_to_str( + self, + operator_sub_trip: tp.Tuple[tp.Tuple[int, int]] + ) -> str: + """Convert the operator sub-trip tuple to a string representation.""" + return "#".join([f"{op_id}_{sub_trip_id}" for op_id, sub_trip_id in operator_sub_trip]) + + def _merge_sub_trip_offers(self) -> tp.Dict[str, tp.Any]: + """Merge sub-trip offers: calculate totals and map specific attributes.""" + # State -> [(SubTripID, WaitKey, DriveKey)] + amod_state_mapping = { + RQ_MODAL_STATE.FIRSTMILE: [ + (RQ_SUB_TRIP_ID.FM_AMOD.value, G_IM_OFFER_FM_WAIT, G_IM_OFFER_FM_DRIVE), + (RQ_SUB_TRIP_ID.FM_PT.value, G_IM_OFFER_PT_WAIT, G_IM_OFFER_PT_DRIVE) + ], + RQ_MODAL_STATE.LASTMILE: [ + (RQ_SUB_TRIP_ID.LM_AMOD.value, G_IM_OFFER_LM_WAIT, G_IM_OFFER_LM_DRIVE), + (RQ_SUB_TRIP_ID.LM_PT.value, G_IM_OFFER_PT_WAIT, G_IM_OFFER_PT_DRIVE) + ], + RQ_MODAL_STATE.FIRSTLASTMILE: [ + (RQ_SUB_TRIP_ID.FLM_AMOD_0.value, G_IM_OFFER_FLM_WAIT_0, G_IM_OFFER_FLM_DRIVE_0), + (RQ_SUB_TRIP_ID.FLM_PT.value, G_IM_OFFER_PT_WAIT, G_IM_OFFER_PT_DRIVE), + (RQ_SUB_TRIP_ID.FLM_AMOD_1.value, G_IM_OFFER_FLM_WAIT_1, G_IM_OFFER_FLM_DRIVE_1) + ] + } + + pt_attributes_to_extract = [G_PT_OFFER_NUM_TRANSFERS] + + # calculate totals + operator_sub_trip_list = [] + total_fare = 0 + total_wait = 0 + total_drive = 0 + + for sub_trip_id, sub_trip_offer in self.sub_trip_offers.items(): + operator_sub_trip_list.append((sub_trip_offer.operator_id, sub_trip_id)) + total_fare += sub_trip_offer.get(G_OFFER_FARE, 0) + total_wait += sub_trip_offer.get(G_OFFER_WAIT, 0) + total_drive += sub_trip_offer.get(G_OFFER_DRIVE, 0) + + # map amod attributes into additional parameters + mapping_configs = amod_state_mapping.get(self.rq_modal_state, []) + + for sub_trip_id, wait_key, drive_key in mapping_configs: + offer = self.sub_trip_offers.get(sub_trip_id, {}) + # map pt specific attributes + if sub_trip_id == RQ_SUB_TRIP_ID.FM_PT.value or sub_trip_id == RQ_SUB_TRIP_ID.LM_PT.value or sub_trip_id == RQ_SUB_TRIP_ID.FLM_PT.value: + for attr in pt_attributes_to_extract: + self.additional_offer_parameters[attr] = offer.get(attr) + self.additional_offer_parameters[wait_key] = offer.get(G_OFFER_WAIT) + self.additional_offer_parameters[drive_key] = offer.get(G_OFFER_DRIVE) + + duration = total_wait + total_drive + self.additional_offer_parameters[G_IM_OFFER_DURATION] = duration + + return { + G_IM_OFFER_OPERATOR_SUB_TRIP_TUPLE: tuple(operator_sub_trip_list), + G_OFFER_FARE: total_fare, + G_OFFER_WAIT: total_wait, + G_OFFER_DRIVE: total_drive, + } \ No newline at end of file diff --git a/src/simulation/Vehicles.py b/src/simulation/Vehicles.py index 5902b017..7e562596 100644 --- a/src/simulation/Vehicles.py +++ b/src/simulation/Vehicles.py @@ -14,7 +14,7 @@ if tp.TYPE_CHECKING: from src.demand.TravelerModels import RequestBase - from src.routing.NetworkBase import NetworkBase + from src.routing.road.NetworkBase import NetworkBase from src.fleetctrl.FleetControlBase import FleetControlBase LOG = logging.getLogger(__name__) @@ -339,8 +339,8 @@ def assign_vehicle_plan(self, list_route_legs : tp.List[VehicleRouteLeg], sim_ti # transform rq from PlanRequest to SimulationRequest (based on RequestBase) # LOG.info(f"Vehicle {self.vid} before new assignment: {[str(x) for x in self.assigned_route]} at time {sim_time}") for vrl in list_route_legs: - boarding_list = [self.rq_db[prq.get_rid()] for prq in vrl.rq_dict.get(1,[])] - alighting_list = [self.rq_db[prq.get_rid()] for prq in vrl.rq_dict.get(-1,[])] + boarding_list = [self.rq_db[prq.get_rid_struct()] for prq in vrl.rq_dict.get(1,[])] + alighting_list = [self.rq_db[prq.get_rid_struct()] for prq in vrl.rq_dict.get(-1,[])] vrl.rq_dict = {1:boarding_list, -1:alighting_list} LOG.debug(f"Vehicle {self.vid} received new VRLs {[str(x) for x in list_route_legs]} at time {sim_time}") LOG.debug(f" -> current assignment: {[str(x) for x in self.assigned_route]}") diff --git a/studies/example_study/scenarios/constant_config_sod.csv b/studies/example_study/scenarios/constant_config_sod.csv old mode 100755 new mode 100644 diff --git a/studies/example_study/scenarios/example_im.csv b/studies/example_study/scenarios/example_im.csv new file mode 100644 index 00000000..d6380964 --- /dev/null +++ b/studies/example_study/scenarios/example_im.csv @@ -0,0 +1,5 @@ +scenario_name,op_module,rq_file,op_fleet_composition,broker_type,broker_maas_detour_time_factor,pt_operator_type,gtfs_name,pt_simulation_start_date,rq_type,op_rh_reservation_max_routes,evaluation_method,sim_env,user_max_decision_time +example_im_ptbroker,PoolingIRSOnly,example_100_intermodal.csv,default_vehtype:5,PTBroker,,PTControlBasic,example_gtfs,20250101,BasicIntermodalRequest,5,intermodal_evaluation,ImmediateDecisionsSimulation,0 +example_im_ptbroker_lmwt30,PoolingIRSOnly,example_100_intermodal_lmwt30.csv,default_vehtype:5,PTBroker,,PTControlBasic,example_gtfs,20250101,BasicIntermodalRequest,5,intermodal_evaluation,ImmediateDecisionsSimulation,0 +example_im_ptbrokerEI_mdtf50,PoolingIRSOnly,example_100_intermodal.csv,default_vehtype:5,PTBrokerEI,50,PTControlBasic,example_gtfs,20250101,BasicIntermodalRequest,5,intermodal_evaluation,ImmediateDecisionsSimulation,0 +example_im_ptbrokerPAYG,PoolingIRSOnly,example_100_intermodal.csv,default_vehtype:5,PTBrokerPAYG,,PTControlBasic,example_gtfs,20250101,BasicIntermodalRequest,5,intermodal_evaluation,ImmediateDecisionsSimulation,0 \ No newline at end of file diff --git a/studies/module_tests/run_module_tests.py b/studies/module_tests/run_module_tests.py index 1cfb2eed..654c3f17 100644 --- a/studies/module_tests/run_module_tests.py +++ b/studies/module_tests/run_module_tests.py @@ -269,6 +269,13 @@ def run_module_test_simulations(N_PARALLEL_SIM=1): sc = os.path.join(scs_path, "sc_config_forecasting.csv") run_scenarios(cc, sc, log_level=log_level, n_cpu_per_sim=1, n_parallel_sim=N_PARALLEL_SIM) print(" => Test Forecasting Modules completed!") + + # Test Intermodal Modules + # TODO: integrate result comparison after fixing issues with intermodal evaluation + print("Test Intermodal Modules ...") + sc = os.path.join(scs_path, "sc_config_im.csv") + run_scenarios(cc, sc, log_level=log_level, n_cpu_per_sim=1, n_parallel_sim=N_PARALLEL_SIM) + print(" => Test Intermodal Modules completed!") # Test SoD Max Modules # TODO test after upgrade diff --git a/studies/module_tests/scenarios/sc_config_im.csv b/studies/module_tests/scenarios/sc_config_im.csv new file mode 100644 index 00000000..61cdc01f --- /dev/null +++ b/studies/module_tests/scenarios/sc_config_im.csv @@ -0,0 +1,3 @@ +scenario_name,op_module,rq_file,op_fleet_composition,broker_type,pt_operator_type,gtfs_name,pt_simulation_start_date,rq_type,op_rh_reservation_max_routes,broker_tpcs_use_default,evaluation_method,sim_env,user_max_decision_time +mt_im_ptbroker_tpcs,PoolingIRSOnly,example_100_intermodal.csv,default_vehtype:5,PTBrokerTPCS,PTControlBasic,example_gtfs,20250101,BasicIntermodalRequest,5,False,intermodal_evaluation,ImmediateDecisionsSimulation,0 +mt_im_ptbroker_tpcs_default,PoolingIRSOnly,example_100_intermodal.csv,default_vehtype:5,PTBrokerTPCS,PTControlBasic,example_gtfs,20250101,BasicIntermodalRequest,5,True,intermodal_evaluation,ImmediateDecisionsSimulation,0 \ No newline at end of file